From 52cba1942563d50a8d239d29cc2cc9ae94c7328b Mon Sep 17 00:00:00 2001 From: Ian Hou <45278651+iankhou@users.noreply.github.com> Date: Wed, 29 Jul 2026 15:56:52 -0400 Subject: [PATCH 01/34] feat(cli-integ): also run integ test suites on Windows Adds windowsTestRunsOn to the integ workflow. When set, every integ suite (integ_cli, integ_toolkit-lib, integ_telemetry, integ_init-templates, integ_tool-integrations) is instantiated a second time on a Windows runner, suffixed with _windows, to catch platform-specific regressions in path handling and subprocess spawning. The prepare/build job stays on Linux, so there is no repo checkout on the Windows runners; they only download the built artifacts and run the suites. Steps run under Git Bash so the shared bash step scripts work unchanged. --- .projenrc.ts | 4 ++ projenrc/cdk-cli-integ-tests.ts | 77 ++++++++++++++++++++++++++------- 2 files changed, 66 insertions(+), 15 deletions(-) diff --git a/.projenrc.ts b/.projenrc.ts index b296a11ec..ff565b6f2 100644 --- a/.projenrc.ts +++ b/.projenrc.ts @@ -1769,6 +1769,10 @@ new CdkCliIntegTestsWorkflow(repo, { testEnvironment: TEST_ENVIRONMENT, buildRunsOn: POWERFUL_RUNNER, testRunsOn: POWERFUL_RUNNER, + // Also run the integ suites on Windows to catch platform-specific + // regressions (paths, subprocess spawning). Uses the free standard runner + // for now; switch to a larger runner label once one is provisioned. + windowsTestRunsOn: 'windows-latest', allowUpstreamVersions: [ // cloud-assembly-schema gets referenced under multiple versions diff --git a/projenrc/cdk-cli-integ-tests.ts b/projenrc/cdk-cli-integ-tests.ts index 3f670e331..e596ca782 100644 --- a/projenrc/cdk-cli-integ-tests.ts +++ b/projenrc/cdk-cli-integ-tests.ts @@ -130,6 +130,17 @@ export interface CdkCliIntegTestsWorkflowProps { */ readonly testRunsOn: string; + /** + * If given, additionally run every integ test matrix job on this Windows + * runner (in addition to the `testRunsOn` runner). + * + * The Windows jobs are suffixed with `_windows` and run all steps under Git + * Bash so the shared bash step scripts keep working. + * + * @default - integ tests only run on `testRunsOn` + */ + readonly windowsTestRunsOn?: string; + /** * GitHub environment name for approvals * @@ -449,36 +460,37 @@ export class CdkCliIntegTestsWorkflow extends Component { // Ensure this is an array const additionalNodeVersionsToTest = this.props.additionalNodeVersionsToTest ?? []; - const testJobs = [ + // The integ test suites, defined once and instantiated per platform. + const suites: Array<[string, MatrixIntegTestProps]> = [ // cli-integ-tests - this.addMatrixJob('cli', { + ['cli', { domain: { suite: ['cli-integ-tests'], shards: 12, }, - }), + }], // toolkit-lib - this.addMatrixJob('toolkit-lib', { + ['toolkit-lib', { domain: { suite: [ 'toolkit-lib-integ-tests', ], node: ['lts/*', ...additionalNodeVersionsToTest], }, - }), + }], // telemetry - this.addMatrixJob('telemetry', { + ['telemetry', { domain: { suite: [ 'telemetry-integ-tests', ], }, - }), + }], // init-templates - this.addMatrixJob('init-templates', { + ['init-templates', { domain: { suite: [ 'init-csharp', @@ -497,15 +509,27 @@ export class CdkCliIntegTestsWorkflow extends Component { suite: 'init-typescript-app', node, })), - }), + }], // We are finding that Amplify works on Node 20, but fails on Node >=22.10. Remove the 'lts/*' test and use a Node 20 for now. - this.addMatrixJob('tool-integrations', { + ['tool-integrations', { domain: { suite: ['tool-integrations'], node: ['20'], }, - }), + }], + ]; + + const testJobs = [ + ...suites.map(([name, jobProps]) => this.addMatrixJob(name, jobProps, { + runsOn: this.props.testRunsOn, + })), + ...(this.props.windowsTestRunsOn + ? suites.map(([name, jobProps]) => this.addMatrixJob(name, jobProps, { + runsOn: this.props.windowsTestRunsOn!, + suffix: '_windows', + })) + : []), ]; // Add a job that collates all matrix jobs into a single status @@ -532,12 +556,13 @@ export class CdkCliIntegTestsWorkflow extends Component { }); } - private addMatrixJob(testName: string, props: MatrixIntegTestProps): string { - const jobName = `integ_${testName}`; + private addMatrixJob(testName: string, props: MatrixIntegTestProps, platform: PlatformOptions): string { + const suffix = platform.suffix ?? ''; + const jobName = `integ_${testName}${suffix}`; let shard: any; let shardArg = ''; - let logName = 'logs-${{ matrix.suite }}-${{ matrix.node }}'; + let logName = `logs${suffix}-\${{ matrix.suite }}-\${{ matrix.node }}`; if (props.domain.shards) { shard = Array(props.domain.shards).fill(0).map((_, i) => i + 1); shardArg = ` --shard="\${{ matrix.shard }}/${props.domain.shards}"`; @@ -546,12 +571,19 @@ export class CdkCliIntegTestsWorkflow extends Component { this.workflow.addJob(jobName, { environment: this.props.testEnvironment, - runsOn: [this.props.testRunsOn], + runsOn: [platform.runsOn], needs: [this.JOB_PREPARE], permissions: { contents: github.workflows.JobPermission.READ, idToken: github.workflows.JobPermission.WRITE, }, + // The step scripts are written for bash; on Windows runners use Git Bash + // (preinstalled) so they run unchanged while still exercising Windows. + defaults: { + run: { + shell: 'bash', + }, + }, env: { // Integ tests heavily rely on processing stdout, node warnings (mostly deprecations) are muddying this. // We can disable any warnings here, there's plenty of other places we will see them. @@ -689,3 +721,18 @@ interface MatrixIntegTestProps { readonly exclude?: github.workflows.JobMatrix['exclude']; readonly extraEnv?: Record; } + +interface PlatformOptions { + /** + * The runner label to run this instance of the job on. + */ + readonly runsOn: string; + + /** + * Suffix appended to the job name and log artifact names, to disambiguate + * multiple platform instances of the same suite. + * + * @default - no suffix + */ + readonly suffix?: string; +} From a1f15d2bbaf24aad22c69a2786b50865eac5041b Mon Sep 17 00:00:00 2001 From: Ian Hou <45278651+iankhou@users.noreply.github.com> Date: Wed, 29 Jul 2026 17:13:22 -0400 Subject: [PATCH 02/34] fix(cli-integ): start Verdaccio via node interpreter for Windows pm2's fork mode cannot launch the global `verdaccio` bin on Windows because it is a `.cmd` shim, not a Node script, so the registry never came up and every integ job failed at the publish step. Resolve the JS entrypoint and start it with an explicit `--interpreter node`, which works on all platforms. --- projenrc/cdk-cli-integ-tests.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/projenrc/cdk-cli-integ-tests.ts b/projenrc/cdk-cli-integ-tests.ts index e596ca782..2c3218118 100644 --- a/projenrc/cdk-cli-integ-tests.ts +++ b/projenrc/cdk-cli-integ-tests.ts @@ -303,7 +303,12 @@ export class CdkCliIntegTestsWorkflow extends Component { 'npm install -g verdaccio pm2', 'mkdir -p $HOME/.config/verdaccio', `echo '${JSON.stringify(verdaccioConfig)}' > $HOME/.config/verdaccio/config.yaml`, - 'pm2 start verdaccio -- --config $HOME/.config/verdaccio/config.yaml', + // Start Verdaccio through pm2 by pointing at its JS entrypoint with an + // explicit Node interpreter. On Windows the global `verdaccio` bin is a + // `.cmd` shim, which pm2's fork mode cannot execute; the resolved JS + // file works on every platform. + 'VERDACCIO_BIN="$(npm root -g)/verdaccio/bin/verdaccio"', + 'pm2 start "$VERDACCIO_BIN" --interpreter node -- --config $HOME/.config/verdaccio/config.yaml', 'sleep 5', // Wait for Verdaccio to start // Configure NPM to use local registry 'echo \'//localhost:4873/:_authToken="MWRjNDU3OTE1NTljYWUyOTFkMWJkOGUyYTIwZWMwNTI6YTgwZjkyNDE0NzgwYWQzNQ=="\' > ~/.npmrc', From c3d328030c4ac036641a3448c1b952c57bfc53fd Mon Sep 17 00:00:00 2001 From: Ian Hou <45278651+iankhou@users.noreply.github.com> Date: Thu, 30 Jul 2026 09:55:39 -0400 Subject: [PATCH 03/34] fix(cli-integ): make test harness work on Windows Three POSIX-isms in the test harness broke every Windows integ run: - cloneDirectory() shelled out to `rm -rf`/`mkdir -p`/`cp -R`, which do not exist under cmd.exe. Replaced with fs.promises equivalents. - addToShellPath() split and joined PATH with ':'; on Windows the delimiter is ';', so prepending the CLI bin dir corrupted the PATH and `cdk` was never found. Use path.delimiter. - A few tests shelled out to `rm`, `cat` (with globs), and `diff`; replaced with portable fs-based implementations. --- .../@aws-cdk-testing/cli-integ/lib/shell.ts | 4 +-- .../cli-integ/lib/with-cdk-app.ts | 9 +++--- ...nerating-and-loading-assembly.integtest.ts | 29 +++++++++++++++++-- ...isk-contain-metadata-resource.integtest.ts | 23 +++++++++++---- .../init-typescript-app.integtest.ts | 2 +- 5 files changed, 53 insertions(+), 14 deletions(-) diff --git a/packages/@aws-cdk-testing/cli-integ/lib/shell.ts b/packages/@aws-cdk-testing/cli-integ/lib/shell.ts index 436ee7633..1be3c53f4 100644 --- a/packages/@aws-cdk-testing/cli-integ/lib/shell.ts +++ b/packages/@aws-cdk-testing/cli-integ/lib/shell.ts @@ -310,13 +310,13 @@ export function rimraf(fsPath: string): boolean { } export function addToShellPath(x: string) { - const parts = process.env.PATH?.split(':') ?? []; + const parts = process.env.PATH?.split(path.delimiter) ?? []; if (!parts.includes(x)) { parts.unshift(x); } - process.env.PATH = parts.join(':'); + process.env.PATH = parts.join(path.delimiter); } /** diff --git a/packages/@aws-cdk-testing/cli-integ/lib/with-cdk-app.ts b/packages/@aws-cdk-testing/cli-integ/lib/with-cdk-app.ts index 5923a445e..44c925ab1 100644 --- a/packages/@aws-cdk-testing/cli-integ/lib/with-cdk-app.ts +++ b/packages/@aws-cdk-testing/cli-integ/lib/with-cdk-app.ts @@ -13,7 +13,7 @@ import type { ITestCliSource, ITestLibrarySource } from './package-sources/sourc import { testSource } from './package-sources/subprocess'; import { RESOURCES_DIR } from './resources'; import type { ShellOptions } from './shell'; -import { shell, ShellHelper, rimraf } from './shell'; +import { ShellHelper, rimraf } from './shell'; import type { AwsContext, AwsContextOptions } from './with-aws'; import { atmosphereEnabled, withAws } from './with-aws'; import { withTimeout } from './with-timeout'; @@ -279,9 +279,10 @@ export interface CdkDestroyCliOptions extends CdkCliOptions { * Prepare a target dir byreplicating a source directory */ export async function cloneDirectory(source: string, target: string, output?: NodeJS.WritableStream) { - await shell(['rm', '-rf', target], { outputs: output ? [output] : [] }); - await shell(['mkdir', '-p', target], { outputs: output ? [output] : [] }); - await shell(['cp', '-R', source + '/*', target], { outputs: output ? [output] : [] }); + output?.write(`Cloning ${source} into ${target}\n`); + await fs.promises.rm(target, { recursive: true, force: true }); + await fs.promises.mkdir(target, { recursive: true }); + await fs.promises.cp(source, target, { recursive: true }); } interface CommonCdkBootstrapCommandOptions { diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/deploy/cdk-generating-and-loading-assembly.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/deploy/cdk-generating-and-loading-assembly.integtest.ts index b4106c500..893afa16f 100644 --- a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/deploy/cdk-generating-and-loading-assembly.integtest.ts +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/deploy/cdk-generating-and-loading-assembly.integtest.ts @@ -7,14 +7,14 @@ integTest( 'generating and loading assembly', withDefaultFixture(async (fixture) => { const asmOutputDir = `${fixture.integTestDir}-cdk-integ-asm`; - await fixture.shell(['rm', '-rf', asmOutputDir]); + await fs.rm(asmOutputDir, { recursive: true, force: true }); // Synthesize a Cloud Assembly tothe default directory (cdk.out) and a specific directory. await fixture.cdk(['synth']); await fixture.cdk(['synth', '--output', asmOutputDir]); // cdk.out in the current directory and the indicated --output should be the same - await fixture.shell(['diff', 'cdk.out', asmOutputDir]); + await assertDirsEqual(path.join(fixture.integTestDir, 'cdk.out'), asmOutputDir); // Check that we can 'ls' the synthesized asm. // Change to some random directory to make sure we're not accidentally loading cdk.json @@ -48,3 +48,28 @@ integTest( }), ); +/** + * Assert that two directories have the same files with the same contents (like `diff -r`) + */ +async function assertDirsEqual(dirA: string, dirB: string) { + const filesA = await relativeFiles(dirA); + const filesB = await relativeFiles(dirB); + expect(filesB).toEqual(filesA); + + for (const file of filesA) { + const contentsA = await fs.readFile(path.join(dirA, file), 'utf-8'); + const contentsB = await fs.readFile(path.join(dirB, file), 'utf-8'); + if (contentsA !== contentsB) { + throw new Error(`File ${file} differs between ${dirA} and ${dirB}`); + } + } +} + +async function relativeFiles(root: string): Promise { + const entries = await fs.readdir(root, { recursive: true, withFileTypes: true }); + return entries + .filter((e) => e.isFile()) + .map((e) => path.join(path.relative(root, e.parentPath), e.name)) + .sort(); +} + diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/synth/cdk-templates-on-disk-contain-metadata-resource.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/synth/cdk-templates-on-disk-contain-metadata-resource.integtest.ts index 8587a51ab..448eb30ff 100644 --- a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/synth/cdk-templates-on-disk-contain-metadata-resource.integtest.ts +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/synth/cdk-templates-on-disk-contain-metadata-resource.integtest.ts @@ -1,3 +1,5 @@ +import { promises as fs } from 'fs'; +import * as path from 'path'; import { integTest, withDefaultFixture } from '../../../lib'; integTest( @@ -7,17 +9,28 @@ integTest( await fixture.cdk(['synth', '--version-reporting=true']); // Load template from disk from root assembly - const templateContents = await fixture.shell(['cat', 'cdk.out/*-lambda.template.json']); + const templateContents = await readMatchingFile(path.join(fixture.integTestDir, 'cdk.out'), /-lambda\.template\.json$/); expect(JSON.parse(templateContents).Resources.CDKMetadata).toBeTruthy(); // Load template from nested assembly - const nestedTemplateContents = await fixture.shell([ - 'cat', - 'cdk.out/assembly-*-stage/*StackInStage*.template.json', - ]); + const assemblyDir = await findMatchingFile(path.join(fixture.integTestDir, 'cdk.out'), /^assembly-.*-stage$/); + const nestedTemplateContents = await readMatchingFile(assemblyDir, /StackInStage.*\.template\.json$/); expect(JSON.parse(nestedTemplateContents).Resources.CDKMetadata).toBeTruthy(); }), ); +async function findMatchingFile(dir: string, pattern: RegExp): Promise { + const entries = await fs.readdir(dir); + const match = entries.find((e) => pattern.test(e)); + if (!match) { + throw new Error(`No file matching ${pattern} found in ${dir}`); + } + return path.join(dir, match); +} + +async function readMatchingFile(dir: string, pattern: RegExp): Promise { + return fs.readFile(await findMatchingFile(dir, pattern), 'utf-8'); +} + diff --git a/packages/@aws-cdk-testing/cli-integ/tests/init-typescript-app/init-typescript-app.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/init-typescript-app/init-typescript-app.integtest.ts index 83025e56f..f87e8b76f 100644 --- a/packages/@aws-cdk-testing/cli-integ/tests/init-typescript-app/init-typescript-app.integtest.ts +++ b/packages/@aws-cdk-testing/cli-integ/tests/init-typescript-app/init-typescript-app.integtest.ts @@ -55,7 +55,7 @@ TYPESCRIPT_VERSIONS.forEach(tsVersion => { await shell.shell(['npm', 'ls']); // this will fail if we have unmet peer dependencies // We just removed the 'jest' dependency so remove the tests as well because they won't compile - await shell.shell(['rm', '-rf', 'test/']); + await fs.rm(path.join(context.integTestDir, 'test'), { recursive: true, force: true }); await shell.shell(['npm', 'run', 'build']); await shell.shell(['cdk', 'synth']); From cb60a8effc36cc1b7477938d32b2a59d31004058 Mon Sep 17 00:00:00 2001 From: Ian Hou <45278651+iankhou@users.noreply.github.com> Date: Thu, 30 Jul 2026 10:24:08 -0400 Subject: [PATCH 04/34] fix(cli-integ): spawn npm through node in typescript version lookups spawnSync('npm', ...) fails on Windows because npm is a .cmd shim, not an executable; JSON.parse then chokes on the undefined stdout. Resolve the npm JS entrypoint and invoke it through process.execPath, matching how the rest of the harness already calls npm. --- packages/@aws-cdk-testing/cli-integ/lib/npm.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/@aws-cdk-testing/cli-integ/lib/npm.ts b/packages/@aws-cdk-testing/cli-integ/lib/npm.ts index 82c96a5f8..a2a20251a 100644 --- a/packages/@aws-cdk-testing/cli-integ/lib/npm.ts +++ b/packages/@aws-cdk-testing/cli-integ/lib/npm.ts @@ -40,7 +40,8 @@ export async function npmQueryInstalledVersion(packageName: string, dir: string) * Use NPM preinstalled on the machine to look up a list of TypeScript versions */ export function typescriptVersionsSync(): string[] { - const { stdout } = spawnSync('npm', ['--silent', 'view', `typescript@>=${MINIMUM_VERSION}`, 'version', '--json'], { encoding: 'utf-8' }); + // Invoke npm through Node: on Windows `npm` is a `.cmd` file, which spawnSync cannot execute directly + const { stdout } = spawnSync(process.execPath, [require.resolve('npm'), '--silent', 'view', `typescript@>=${MINIMUM_VERSION}`, 'version', '--json'], { encoding: 'utf-8' }); const versions: string[] = JSON.parse(stdout); return Array.from(new Set(versions.map(v => v.split('.').slice(0, 2).join('.')))); @@ -50,7 +51,7 @@ export function typescriptVersionsSync(): string[] { * Use NPM preinstalled on the machine to query publish times of versions */ export function typescriptVersionsYoungerThanDaysSync(days: number, versions: string[]): string[] { - const { stdout } = spawnSync('npm', ['--silent', 'view', 'typescript', 'time', '--json'], { encoding: 'utf-8' }); + const { stdout } = spawnSync(process.execPath, [require.resolve('npm'), '--silent', 'view', 'typescript', 'time', '--json'], { encoding: 'utf-8' }); const versionTsMap: Record = JSON.parse(stdout); const cutoffDate = new Date(Date.now() - (days * 24 * 3600 * 1000)); From c0bade5c548cd6b736d5136dd7430869bcbfd4f8 Mon Sep 17 00:00:00 2001 From: Ian Hou <45278651+iankhou@users.noreply.github.com> Date: Thu, 30 Jul 2026 10:31:00 -0400 Subject: [PATCH 05/34] fix(cli-integ): search all stage assemblies for nested template The portable replacement for `cat cdk.out/assembly-*-stage/*StackInStage*` picked the first directory matching assembly-*-stage, which can be the bundling stage that contains no StackInStage template. Search recursively for the full relative path instead, mirroring the original glob semantics. --- ...isk-contain-metadata-resource.integtest.ts | 28 +++++++++++-------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/synth/cdk-templates-on-disk-contain-metadata-resource.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/synth/cdk-templates-on-disk-contain-metadata-resource.integtest.ts index 448eb30ff..082d1db5f 100644 --- a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/synth/cdk-templates-on-disk-contain-metadata-resource.integtest.ts +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/synth/cdk-templates-on-disk-contain-metadata-resource.integtest.ts @@ -9,28 +9,34 @@ integTest( await fixture.cdk(['synth', '--version-reporting=true']); // Load template from disk from root assembly - const templateContents = await readMatchingFile(path.join(fixture.integTestDir, 'cdk.out'), /-lambda\.template\.json$/); + const templateContents = await readMatchingFile(path.join(fixture.integTestDir, 'cdk.out'), /^[^\\/]*-lambda\.template\.json$/); expect(JSON.parse(templateContents).Resources.CDKMetadata).toBeTruthy(); - // Load template from nested assembly - const assemblyDir = await findMatchingFile(path.join(fixture.integTestDir, 'cdk.out'), /^assembly-.*-stage$/); - const nestedTemplateContents = await readMatchingFile(assemblyDir, /StackInStage.*\.template\.json$/); + // Load template from nested assembly (multiple stage assemblies exist; find the one holding StackInStage) + const nestedTemplate = await findMatchingFile( + path.join(fixture.integTestDir, 'cdk.out'), + /^assembly-.*-stage[\\/].*StackInStage.*\.template\.json$/, + ); + const nestedTemplateContents = await fs.readFile(nestedTemplate, 'utf-8'); expect(JSON.parse(nestedTemplateContents).Resources.CDKMetadata).toBeTruthy(); }), ); -async function findMatchingFile(dir: string, pattern: RegExp): Promise { - const entries = await fs.readdir(dir); - const match = entries.find((e) => pattern.test(e)); +/** + * Find a file whose path relative to `root` matches `pattern`, searching recursively (like a shell glob) + */ +async function findMatchingFile(root: string, pattern: RegExp): Promise { + const entries = await fs.readdir(root, { recursive: true, withFileTypes: true }); + const match = entries.find((e) => e.isFile() && pattern.test(path.join(path.relative(root, e.parentPath), e.name))); if (!match) { - throw new Error(`No file matching ${pattern} found in ${dir}`); + throw new Error(`No file matching ${pattern} found in ${root}`); } - return path.join(dir, match); + return path.join(match.parentPath, match.name); } -async function readMatchingFile(dir: string, pattern: RegExp): Promise { - return fs.readFile(await findMatchingFile(dir, pattern), 'utf-8'); +async function readMatchingFile(root: string, pattern: RegExp): Promise { + return fs.readFile(await findMatchingFile(root, pattern), 'utf-8'); } From a27886e4fd7c3f6b3d053954e9d80e22f6aa5d9d Mon Sep 17 00:00:00 2001 From: Ian Hou <45278651+iankhou@users.noreply.github.com> Date: Thu, 30 Jul 2026 10:53:08 -0400 Subject: [PATCH 06/34] fix(cli-integ): Windows fixes for docker login env expansion and python venv layout - ecrPublicLogin passed the ECR password as a literal `${ECR_PASSWORD}`, which cmd.exe does not expand, so `docker login` sent the literal string and got a 400. Use `%ECR_PASSWORD%` on Windows. This failed every test using the default fixture (~200 tests) since the fixture logs in at setup. - init-python looked for the virtualenv binaries in `.venv/bin`; on Windows virtualenv creates `.venv/Scripts`. Also use path.delimiter for PATH. --- .../@aws-cdk-testing/cli-integ/lib/with-cdk-app.ts | 7 ++++++- .../tests/init-python/init-python.integtest.ts | 10 ++++++---- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/packages/@aws-cdk-testing/cli-integ/lib/with-cdk-app.ts b/packages/@aws-cdk-testing/cli-integ/lib/with-cdk-app.ts index 44c925ab1..62825da75 100644 --- a/packages/@aws-cdk-testing/cli-integ/lib/with-cdk-app.ts +++ b/packages/@aws-cdk-testing/cli-integ/lib/with-cdk-app.ts @@ -515,9 +515,14 @@ export class TestFixture extends ShellHelper { const decoded = Buffer.from(authData, 'base64').toString('utf-8'); const [username, password] = decoded.split(':'); + // Reference the password via an environment variable so it doesn't leak into + // process listings. The expansion syntax depends on the shell interpreting it + // (cmd.exe on Windows, /bin/sh elsewhere). + const passwordRef = process.platform === 'win32' ? '%ECR_PASSWORD%' : '${ECR_PASSWORD}'; + await this.shell([docker, 'login', '--username', username, - '--password', '${ECR_PASSWORD}', + '--password', passwordRef, 'public.ecr.aws'], { shell: true, modEnv: { diff --git a/packages/@aws-cdk-testing/cli-integ/tests/init-python/init-python.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/init-python/init-python.integtest.ts index 4e4a89b22..4e2f8461b 100644 --- a/packages/@aws-cdk-testing/cli-integ/tests/init-python/init-python.integtest.ts +++ b/packages/@aws-cdk-testing/cli-integ/tests/init-python/init-python.integtest.ts @@ -10,11 +10,13 @@ import { integTest, withTemporaryDirectory, ShellHelper, withPackages } from '.. await shell.shell(['cdk', 'init', '--lib-version', context.library.requestedVersion(), '-l', 'python', template]); const venvPath = path.resolve(context.integTestDir, '.venv'); - const venv = { PATH: `${venvPath}/bin:${process.env.PATH}`, VIRTUAL_ENV: venvPath }; + // Virtualenvs put binaries in 'Scripts' on Windows and 'bin' elsewhere + const venvBin = path.join(venvPath, process.platform === 'win32' ? 'Scripts' : 'bin'); + const venv = { PATH: `${venvBin}${path.delimiter}${process.env.PATH}`, VIRTUAL_ENV: venvPath }; - await shell.shell([`${venvPath}/bin/pip`, 'install', '-r', 'requirements.txt'], { modEnv: venv }); - await shell.shell([`${venvPath}/bin/pip`, 'install', '-r', 'requirements-dev.txt'], { modEnv: venv }); - await shell.shell([`${venvPath}/bin/pytest`], { modEnv: venv }); + await shell.shell([path.join(venvBin, 'pip'), 'install', '-r', 'requirements.txt'], { modEnv: venv }); + await shell.shell([path.join(venvBin, 'pip'), 'install', '-r', 'requirements-dev.txt'], { modEnv: venv }); + await shell.shell([path.join(venvBin, 'pytest')], { modEnv: venv }); await shell.shell(['cdk', 'synth'], { modEnv: venv }); }))); }); From e8adbe39ee6e26528c93b6972ac571af353852de Mon Sep 17 00:00:00 2001 From: Ian Hou <45278651+iankhou@users.noreply.github.com> Date: Thu, 30 Jul 2026 11:24:24 -0400 Subject: [PATCH 07/34] fix(cli-integ): disable wincred credential helper for docker login on Windows The %ECR_PASSWORD% expansion fix got docker login past authentication, but storing the credential then failed with 'error storing credentials - err: exit status 1, out: The stub received bad data' from the wincred helper, which does not handle ~40 parallel logins from concurrent test workers. Write {"credsStore": ""} into the per-test DOCKER_CONFIG dir so credentials go to the config file, matching Linux runner behavior. --- packages/@aws-cdk-testing/cli-integ/lib/with-cdk-app.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/packages/@aws-cdk-testing/cli-integ/lib/with-cdk-app.ts b/packages/@aws-cdk-testing/cli-integ/lib/with-cdk-app.ts index 62825da75..45f36045d 100644 --- a/packages/@aws-cdk-testing/cli-integ/lib/with-cdk-app.ts +++ b/packages/@aws-cdk-testing/cli-integ/lib/with-cdk-app.ts @@ -520,6 +520,15 @@ export class TestFixture extends ShellHelper { // (cmd.exe on Windows, /bin/sh elsewhere). const passwordRef = process.platform === 'win32' ? '%ECR_PASSWORD%' : '${ECR_PASSWORD}'; + // On Windows, Docker defaults to the 'wincred' credential helper, which fails + // under concurrent logins from parallel tests ('The stub received bad data'). + // Disable it so credentials are stored in the per-test config file, matching + // the behavior on Linux runners (which have no credential helper installed). + if (process.platform === 'win32') { + await fs.promises.mkdir(this.dockerConfigDir, { recursive: true }); + await fs.promises.writeFile(path.join(this.dockerConfigDir, 'config.json'), JSON.stringify({ credsStore: '' })); + } + await this.shell([docker, 'login', '--username', username, '--password', passwordRef, From 4da22c7d6c4c4e7566da0052d0f7a7c9bcd8beeb Mon Sep 17 00:00:00 2001 From: Ian Hou <45278651+iankhou@users.noreply.github.com> Date: Thu, 30 Jul 2026 11:55:58 -0400 Subject: [PATCH 08/34] fix(cli-integ): raise init test timeouts to 5 minutes The init suites (java, go, python, csharp, fsharp, javascript, typescript-lib) ran with jest's suite default of 60s. On Windows runners a `cdk init` plus toolchain build (mvn package, go test, pip install) takes 3-5 minutes, so every test timed out. Worse, jest keeps the timed-out test body running while it starts the retry, and the zombie's subsequent commands fail confusingly. init-typescript-app already used 300s; apply the same to the rest. --- .../cli-integ/tests/init-csharp/init-csharp.integtest.ts | 2 +- .../cli-integ/tests/init-fsharp/init-fsharp.integtest.ts | 2 +- .../cli-integ/tests/init-go/init-go.integtest.ts | 2 +- .../cli-integ/tests/init-java/init-java.integtest.ts | 2 +- .../tests/init-javascript/init-javascript.integtest.ts | 4 ++-- .../cli-integ/tests/init-python/init-python.integtest.ts | 2 +- .../init-typescript-lib/init-typescript-lib.integtest.ts | 2 +- .../use-lib-as-bundled-dependency.integtest.ts | 2 +- 8 files changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/@aws-cdk-testing/cli-integ/tests/init-csharp/init-csharp.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/init-csharp/init-csharp.integtest.ts index 98fc4da23..3af10939d 100644 --- a/packages/@aws-cdk-testing/cli-integ/tests/init-csharp/init-csharp.integtest.ts +++ b/packages/@aws-cdk-testing/cli-integ/tests/init-csharp/init-csharp.integtest.ts @@ -10,6 +10,6 @@ import { integTest, withTemporaryDirectory, ShellHelper, withPackages } from '.. await shell.shell(['cdk', 'init', '--lib-version', context.library.requestedVersion(), '-l', 'csharp', template]); await context.library.initializeDotnetPackages(context.integTestDir); await shell.shell(['cdk', 'synth']); - }))); + })), 300_000); }); diff --git a/packages/@aws-cdk-testing/cli-integ/tests/init-fsharp/init-fsharp.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/init-fsharp/init-fsharp.integtest.ts index b53b28a91..d7d96e032 100644 --- a/packages/@aws-cdk-testing/cli-integ/tests/init-fsharp/init-fsharp.integtest.ts +++ b/packages/@aws-cdk-testing/cli-integ/tests/init-fsharp/init-fsharp.integtest.ts @@ -10,6 +10,6 @@ import { integTest, withTemporaryDirectory, ShellHelper, withPackages } from '.. await shell.shell(['cdk', 'init', '--lib-version', context.library.requestedVersion(), '-l', 'fsharp', template]); await context.library.initializeDotnetPackages(context.integTestDir); await shell.shell(['cdk', 'synth']); - }))); + })), 300_000); }); diff --git a/packages/@aws-cdk-testing/cli-integ/tests/init-go/init-go.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/init-go/init-go.integtest.ts index cd256f723..8f501d1c4 100644 --- a/packages/@aws-cdk-testing/cli-integ/tests/init-go/init-go.integtest.ts +++ b/packages/@aws-cdk-testing/cli-integ/tests/init-go/init-go.integtest.ts @@ -25,5 +25,5 @@ import { integTest, withTemporaryDirectory, ShellHelper, withPackages } from '.. await shell.shell(['go', 'test']); await shell.shell(['cdk', 'synth']); - }))); + })), 300_000); }); diff --git a/packages/@aws-cdk-testing/cli-integ/tests/init-java/init-java.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/init-java/init-java.integtest.ts index dbeedda4e..45d5dead0 100644 --- a/packages/@aws-cdk-testing/cli-integ/tests/init-java/init-java.integtest.ts +++ b/packages/@aws-cdk-testing/cli-integ/tests/init-java/init-java.integtest.ts @@ -10,5 +10,5 @@ import { integTest, withTemporaryDirectory, ShellHelper, withPackages } from '.. await shell.shell(['cdk', 'init', '--lib-version', context.library.requestedVersion(), '-l', 'java', template]); await shell.shell(['mvn', 'package']); await shell.shell(['cdk', 'synth']); - }))); + })), 300_000); }); diff --git a/packages/@aws-cdk-testing/cli-integ/tests/init-javascript/init-javascript.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/init-javascript/init-javascript.integtest.ts index 1e01e9767..38b9e2014 100644 --- a/packages/@aws-cdk-testing/cli-integ/tests/init-javascript/init-javascript.integtest.ts +++ b/packages/@aws-cdk-testing/cli-integ/tests/init-javascript/init-javascript.integtest.ts @@ -13,7 +13,7 @@ import { integTest, withTemporaryDirectory, ShellHelper, withPackages } from '.. await shell.shell(['npm', 'run', 'test']); await shell.shell(['cdk', 'synth']); - }))); + })), 300_000); }); integTest('Test importing CDK from ESM', withTemporaryDirectory(withPackages(async (context) => { @@ -55,4 +55,4 @@ new TestjsStack(app, 'TestjsStack'); await fs.writeJson(path.join(context.integTestDir, 'cdk.json'), cdkJson); await shell.shell(['cdk', 'synth']); -}))); +})), 300_000); diff --git a/packages/@aws-cdk-testing/cli-integ/tests/init-python/init-python.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/init-python/init-python.integtest.ts index 4e2f8461b..075671f78 100644 --- a/packages/@aws-cdk-testing/cli-integ/tests/init-python/init-python.integtest.ts +++ b/packages/@aws-cdk-testing/cli-integ/tests/init-python/init-python.integtest.ts @@ -18,5 +18,5 @@ import { integTest, withTemporaryDirectory, ShellHelper, withPackages } from '.. await shell.shell([path.join(venvBin, 'pip'), 'install', '-r', 'requirements-dev.txt'], { modEnv: venv }); await shell.shell([path.join(venvBin, 'pytest')], { modEnv: venv }); await shell.shell(['cdk', 'synth'], { modEnv: venv }); - }))); + })), 300_000); }); diff --git a/packages/@aws-cdk-testing/cli-integ/tests/init-typescript-lib/init-typescript-lib.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/init-typescript-lib/init-typescript-lib.integtest.ts index 57d7adfdf..2f73b06ed 100644 --- a/packages/@aws-cdk-testing/cli-integ/tests/init-typescript-lib/init-typescript-lib.integtest.ts +++ b/packages/@aws-cdk-testing/cli-integ/tests/init-typescript-lib/init-typescript-lib.integtest.ts @@ -10,4 +10,4 @@ integTest('typescript init lib', withTemporaryDirectory(withPackages(async (cont await shell.shell(['npm', 'ls']); // this will fail if we have unmet peer dependencies await shell.shell(['npm', 'run', 'build']); await shell.shell(['npm', 'run', 'test']); -}))); +})), 300_000); diff --git a/packages/@aws-cdk-testing/cli-integ/tests/init-typescript-lib/use-lib-as-bundled-dependency.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/init-typescript-lib/use-lib-as-bundled-dependency.integtest.ts index c757fa7e7..9b22aab91 100644 --- a/packages/@aws-cdk-testing/cli-integ/tests/init-typescript-lib/use-lib-as-bundled-dependency.integtest.ts +++ b/packages/@aws-cdk-testing/cli-integ/tests/init-typescript-lib/use-lib-as-bundled-dependency.integtest.ts @@ -22,4 +22,4 @@ integTest('using aws-cdk-lib as a bundled dependency', withTemporaryDirectory(wi await fs.writeFile(packageJsonPath, JSON.stringify(packageJson, undefined, 2), 'utf-8'); await shell.shell(['npm', 'install']); -}))); +})), 300_000); From 38039f34504958d9eca42ecf27db4f3461c85941 Mon Sep 17 00:00:00 2001 From: Ian Hou <45278651+iankhou@users.noreply.github.com> Date: Thu, 30 Jul 2026 12:27:43 -0400 Subject: [PATCH 09/34] fix(cli-integ): write ECR auth directly to docker config on Windows Disabling credsStore in the config file was not enough: docker on Windows auto-detects the wincred helper and still fails to store the ~1.5KB ECR token ('The stub received bad data'). Skip `docker login` entirely on Windows and write the auths entry into the per-test DOCKER_CONFIG config.json ourselves - the same end state docker login produces on the Linux runners. --- .../cli-integ/lib/with-cdk-app.ts | 36 ++++++++++--------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/packages/@aws-cdk-testing/cli-integ/lib/with-cdk-app.ts b/packages/@aws-cdk-testing/cli-integ/lib/with-cdk-app.ts index 45f36045d..bddfc3650 100644 --- a/packages/@aws-cdk-testing/cli-integ/lib/with-cdk-app.ts +++ b/packages/@aws-cdk-testing/cli-integ/lib/with-cdk-app.ts @@ -506,32 +506,36 @@ export class TestFixture extends ShellHelper { const tokenResponse = await this.aws.ecrPublic.send(new GetAuthorizationTokenCommand({})); const authData = tokenResponse.authorizationData?.authorizationToken; - const docker = process.env.CDK_DOCKER ?? 'docker'; - if (!authData) { throw new Error('Could not retrieve ECR public auth token.'); } - const decoded = Buffer.from(authData, 'base64').toString('utf-8'); - const [username, password] = decoded.split(':'); - - // Reference the password via an environment variable so it doesn't leak into - // process listings. The expansion syntax depends on the shell interpreting it - // (cmd.exe on Windows, /bin/sh elsewhere). - const passwordRef = process.platform === 'win32' ? '%ECR_PASSWORD%' : '${ECR_PASSWORD}'; - - // On Windows, Docker defaults to the 'wincred' credential helper, which fails - // under concurrent logins from parallel tests ('The stub received bad data'). - // Disable it so credentials are stored in the per-test config file, matching - // the behavior on Linux runners (which have no credential helper installed). if (process.platform === 'win32') { + // `docker login` on Windows stores credentials through the wincred credential + // helper (auto-detected even if `credsStore` is empty in the config file), and + // wincred cannot store ECR tokens: they exceed Windows Credential Manager's + // 2560-byte limit ('The stub received bad data'). Write the auth directly into + // the per-test Docker config file instead, which is exactly what `docker login` + // produces on the Linux runners, where no credential helper is installed. + // The plaintext `auths` entry takes precedence over any credential helper. await fs.promises.mkdir(this.dockerConfigDir, { recursive: true }); - await fs.promises.writeFile(path.join(this.dockerConfigDir, 'config.json'), JSON.stringify({ credsStore: '' })); + await fs.promises.writeFile( + path.join(this.dockerConfigDir, 'config.json'), + JSON.stringify({ auths: { 'public.ecr.aws': { auth: authData } } }), + ); + return; } + const docker = process.env.CDK_DOCKER ?? 'docker'; + + const decoded = Buffer.from(authData, 'base64').toString('utf-8'); + const [username, password] = decoded.split(':'); + + // Reference the password via an environment variable so it doesn't leak into + // process listings; the shell expands it. await this.shell([docker, 'login', '--username', username, - '--password', passwordRef, + '--password', '${ECR_PASSWORD}', 'public.ecr.aws'], { shell: true, modEnv: { From f0cf15a068bd5f35acc0e9f86c6ad17d453fda41 Mon Sep 17 00:00:00 2001 From: Ian Hou <45278651+iankhou@users.noreply.github.com> Date: Thu, 30 Jul 2026 13:07:06 -0400 Subject: [PATCH 10/34] feat(cli-integ): exclude work directories from Defender on Windows runners Defender real-time scanning hooks every file write. The integ tests are dominated by npm installs and toolchain builds that create tens of thousands of small files, making Windows jobs 3-10x slower than Linux (e.g. tool-integrations: 19-29 min vs 2.5 min). Exclude the workspace, temp, npm and toolcache directories from scanning; the runner VM is ephemeral and job-isolated so this carries no persistent risk. --- projenrc/cdk-cli-integ-tests.ts | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/projenrc/cdk-cli-integ-tests.ts b/projenrc/cdk-cli-integ-tests.ts index 2c3218118..ffde279d2 100644 --- a/projenrc/cdk-cli-integ-tests.ts +++ b/projenrc/cdk-cli-integ-tests.ts @@ -533,6 +533,7 @@ export class CdkCliIntegTestsWorkflow extends Component { ? suites.map(([name, jobProps]) => this.addMatrixJob(name, jobProps, { runsOn: this.props.windowsTestRunsOn!, suffix: '_windows', + windows: true, })) : []), ]; @@ -618,6 +619,15 @@ export class CdkCliIntegTestsWorkflow extends Component { }, }, steps: [ + ...platform.windows ? [{ + // Defender's real-time scanning hooks every file write; npm-heavy tests + // create tens of thousands of small files, and scanning slows them down + // 3-10x. The runner VM is ephemeral and job-isolated, so exclude the + // work, tool and temp directories from scanning. + name: 'Exclude work directories from Windows Defender', + shell: 'powershell', + run: 'Add-MpPreference -ExclusionPath "$env:GITHUB_WORKSPACE", "$env:TEMP", "$env:USERPROFILE", "C:\\npm", "C:\\hostedtoolcache"', + }] : [], github.WorkflowSteps.downloadArtifact({ with: { artifactIds: [`\${{needs.${this.JOB_PREPARE}.outputs.packagesArtifact}}`], @@ -740,4 +750,13 @@ interface PlatformOptions { * @default - no suffix */ readonly suffix?: string; + + /** + * Whether this job runs on a Windows runner. + * + * Adds Windows-specific setup steps. + * + * @default false + */ + readonly windows?: boolean; } From 688abe50fbe21f7ad398edb6eeda408936989240 Mon Sep 17 00:00:00 2001 From: Ian Hou <45278651+iankhou@users.noreply.github.com> Date: Thu, 30 Jul 2026 14:16:02 -0400 Subject: [PATCH 11/34] fix(cli-integ): skip Linux docker tests on Windows, raise typescript-app timeouts - GitHub Windows runners run Docker in Windows-containers mode and cannot pull or build Linux images ('no matching manifest for windows/amd64', or no daemon pipe at all). Skip the 13 tests that build/run Linux images on Windows via CDK_INTEG_SKIP_TESTS; they retain full coverage on Linux. - The typescript-version matrix tests in init-typescript-app still used the 60s suite default and timed out on Windows; the templated init tests hit 300s when several npm installs run concurrently. Raise to 300s/600s. --- .../init-typescript-app.integtest.ts | 4 +-- projenrc/cdk-cli-integ-tests.ts | 26 +++++++++++++++++++ 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/packages/@aws-cdk-testing/cli-integ/tests/init-typescript-app/init-typescript-app.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/init-typescript-app/init-typescript-app.integtest.ts index f87e8b76f..5f8796336 100644 --- a/packages/@aws-cdk-testing/cli-integ/tests/init-typescript-app/init-typescript-app.integtest.ts +++ b/packages/@aws-cdk-testing/cli-integ/tests/init-typescript-app/init-typescript-app.integtest.ts @@ -19,7 +19,7 @@ import { typescriptVersionsSync, typescriptVersionsYoungerThanDaysSync } from '. await shell.shell(['npm', 'run', 'test']); await shell.shell(['cdk', 'synth']); - })), 300_000); + })), 600_000); }); // Same as https://github.com/DefinitelyTyped/DefinitelyTyped?tab=readme-ov-file#support-window @@ -59,7 +59,7 @@ TYPESCRIPT_VERSIONS.forEach(tsVersion => { await shell.shell(['npm', 'run', 'build']); await shell.shell(['cdk', 'synth']); - }))); + })), 300_000); }); async function removeDevDependencies(context: TemporaryDirectoryContext) { diff --git a/projenrc/cdk-cli-integ-tests.ts b/projenrc/cdk-cli-integ-tests.ts index ffde279d2..995f9da97 100644 --- a/projenrc/cdk-cli-integ-tests.ts +++ b/projenrc/cdk-cli-integ-tests.ts @@ -12,6 +12,29 @@ export function fixupTestTask(project: Project, taskName = 'test'): void { const NOT_FLAGGED_EXPR = "!contains(github.event.pull_request.labels.*.name, 'pr/exempt-integ-test')"; +/** + * Tests that build or run Linux Docker images. + * + * GitHub-hosted Windows runners run Docker in Windows-containers mode and + * cannot pull or build Linux images ('no matching manifest for windows/amd64'), + * so these tests are skipped on Windows. + */ +const DOCKER_TESTS_SKIPPED_ON_WINDOWS = [ + 'deploy same docker asset to multiple regions', + 'deploy same docker asset to multiple stacks', + 'deploy stack with multiple docker assets', + 'deploy stack with docker asset', + 'cdk-assets smoke test', + 'deploy new style synthesis to new style bootstrap (with docker image)', + 'Garbage Collection untags in-use ecr images', + 'Garbage Collection keeps in use ecr images', + 'Garbage Collection deletes unused ecr images', + 'Garbage Collection tags unused ecr images', + 'all calls from isolated container go through proxy', + 'docker-credential-cdk-assets can assume role and fetch ECR credentials', + 'toolkit deploy stack with multiple docker assets', +]; + function setupNodeStep(nodeVersion: string): github.workflows.JobStep { return { name: 'Setup Node.js', @@ -600,6 +623,9 @@ export class CdkCliIntegTestsWorkflow extends Component { // assumptions about the availability of source packages. IS_CANARY: 'true', CI: 'true', + ...platform.windows ? { + CDK_INTEG_SKIP_TESTS: DOCKER_TESTS_SKIPPED_ON_WINDOWS.join(','), + } : {}, // add extra env at end so it can override ...props.extraEnv, }, From deef751852e81f5a7acd0c321ee353d5c7a49051 Mon Sep 17 00:00:00 2001 From: Ian Hou <45278651+iankhou@users.noreply.github.com> Date: Thu, 30 Jul 2026 14:44:22 -0400 Subject: [PATCH 12/34] feat(cli-integ): replace Defender exclusion with a Dev Drive for TEMP A/B timing between runs with and without the Defender exclusion showed no measurable difference - runner images evidently already handle it. Replace it with a Dev Drive (ReFS VHDX): all test fixtures live under os.tmpdir(), so pointing TEMP/TMP at the Dev Drive moves the npm-install-heavy file churn onto the faster filesystem. windows-latest is Server 2025 (build 26100), which supports Format-Volume -DevDrive natively. --- projenrc/cdk-cli-integ-tests.ts | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/projenrc/cdk-cli-integ-tests.ts b/projenrc/cdk-cli-integ-tests.ts index 995f9da97..c5ff6195a 100644 --- a/projenrc/cdk-cli-integ-tests.ts +++ b/projenrc/cdk-cli-integ-tests.ts @@ -646,13 +646,20 @@ export class CdkCliIntegTestsWorkflow extends Component { }, steps: [ ...platform.windows ? [{ - // Defender's real-time scanning hooks every file write; npm-heavy tests - // create tens of thousands of small files, and scanning slows them down - // 3-10x. The runner VM is ephemeral and job-isolated, so exclude the - // work, tool and temp directories from scanning. - name: 'Exclude work directories from Windows Defender', + // The integ tests are dominated by npm installs and toolchain builds: + // many small file writes, which are slow on the runner's NTFS OS disk. + // A Dev Drive (ReFS VHDX) is much faster for this pattern. Create one + // and point TEMP at it, which is where all test fixtures live + // (the harness creates its working directories under os.tmpdir()). + name: 'Set up Dev Drive for TEMP', shell: 'powershell', - run: 'Add-MpPreference -ExclusionPath "$env:GITHUB_WORKSPACE", "$env:TEMP", "$env:USERPROFILE", "C:\\npm", "C:\\hostedtoolcache"', + run: [ + '$vhd = "C:\\devdrive.vhdx"', + '$drive = (New-VHD -Path $vhd -SizeBytes 20GB -Dynamic | Mount-VHD -PassThru | Initialize-Disk -PassThru | New-Partition -AssignDriveLetter -UseMaximumSize | Format-Volume -DevDrive -Confirm:$false).DriveLetter', + 'New-Item -ItemType Directory -Path "${drive}:\\temp" | Out-Null', + 'echo "TEMP=${drive}:\\temp" >> $env:GITHUB_ENV', + 'echo "TMP=${drive}:\\temp" >> $env:GITHUB_ENV', + ].join('\n'), }] : [], github.WorkflowSteps.downloadArtifact({ with: { From bce87c2f96ddb1b02a05cad9b409e9d5426054eb Mon Sep 17 00:00:00 2001 From: Ian Hou <45278651+iankhou@users.noreply.github.com> Date: Thu, 30 Jul 2026 16:29:54 -0400 Subject: [PATCH 13/34] feat(cli-integ): extend Dev Drive to npm cache, grow VHDX to 40GB TEMP-only Dev Drive placement showed 25-35% job speedups on npm-heavy suites but left the npm cache on the OS disk, where every npm invocation in the job (global verdaccio install, per-test installs) still pays NTFS tax. Point npm_config_cache at the Dev Drive as well, and grow the dynamically-allocated VHDX to 40GB to fit cache plus concurrent fixtures. --- projenrc/cdk-cli-integ-tests.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/projenrc/cdk-cli-integ-tests.ts b/projenrc/cdk-cli-integ-tests.ts index c5ff6195a..0ba8f699e 100644 --- a/projenrc/cdk-cli-integ-tests.ts +++ b/projenrc/cdk-cli-integ-tests.ts @@ -651,14 +651,18 @@ export class CdkCliIntegTestsWorkflow extends Component { // A Dev Drive (ReFS VHDX) is much faster for this pattern. Create one // and point TEMP at it, which is where all test fixtures live // (the harness creates its working directories under os.tmpdir()). - name: 'Set up Dev Drive for TEMP', + name: 'Set up Dev Drive for TEMP and npm cache', shell: 'powershell', run: [ '$vhd = "C:\\devdrive.vhdx"', - '$drive = (New-VHD -Path $vhd -SizeBytes 20GB -Dynamic | Mount-VHD -PassThru | Initialize-Disk -PassThru | New-Partition -AssignDriveLetter -UseMaximumSize | Format-Volume -DevDrive -Confirm:$false).DriveLetter', + '$drive = (New-VHD -Path $vhd -SizeBytes 40GB -Dynamic | Mount-VHD -PassThru | Initialize-Disk -PassThru | New-Partition -AssignDriveLetter -UseMaximumSize | Format-Volume -DevDrive -Confirm:$false).DriveLetter', 'New-Item -ItemType Directory -Path "${drive}:\\temp" | Out-Null', + 'New-Item -ItemType Directory -Path "${drive}:\\npm-cache" | Out-Null', 'echo "TEMP=${drive}:\\temp" >> $env:GITHUB_ENV', 'echo "TMP=${drive}:\\temp" >> $env:GITHUB_ENV', + // Every npm invocation in the job (global installs, per-test installs) + // reads and writes the cache, so move it onto the Dev Drive too + 'echo "npm_config_cache=${drive}:\\npm-cache" >> $env:GITHUB_ENV', ].join('\n'), }] : [], github.WorkflowSteps.downloadArtifact({ From ea35192d2181dd299fb3b2545ee5ac40464c3aa4 Mon Sep 17 00:00:00 2001 From: Ian Hou <45278651+iankhou@users.noreply.github.com> Date: Thu, 30 Jul 2026 17:10:58 -0400 Subject: [PATCH 14/34] fix(cli-integ): skip four more Linux-image tests on Windows These tests do not have docker in the name but build Linux images as a side effect and fail on Windows runners the same way as the named docker tests: python lambda bundling (sam/build-python3.12 image), SAM metadata asset bundling, a DockerImageAsset in the session-tags fixture stack, and a docker-app deploy from a copied cloud assembly. Their hung docker builds were also occupying jest workers and pushing unrelated tests in the same shard past the 1200s harness timeout. --- projenrc/cdk-cli-integ-tests.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/projenrc/cdk-cli-integ-tests.ts b/projenrc/cdk-cli-integ-tests.ts index 0ba8f699e..9e138a507 100644 --- a/projenrc/cdk-cli-integ-tests.ts +++ b/projenrc/cdk-cli-integ-tests.ts @@ -33,6 +33,13 @@ const DOCKER_TESTS_SKIPPED_ON_WINDOWS = [ 'all calls from isolated container go through proxy', 'docker-credential-cdk-assets can assume role and fetch ECR credentials', 'toolkit deploy stack with multiple docker assets', + // These do not have 'docker' in the name, but build Linux images as a side + // effect: python lambda bundling, SAM asset bundling, a DockerImageAsset in + // the fixture stack, and a docker-app deploy from a copied assembly. + 'CDK synth bundled functions as expected', + 'CDK synth add the metadata properties expected by sam', + 'can deploy with session tags on the deploy, lookup, file asset, and image asset publishing roles', + 'generating and loading assembly', ]; function setupNodeStep(nodeVersion: string): github.workflows.JobStep { From 7dc181d21af46021d1528068d4b10033e9c4c939 Mon Sep 17 00:00:00 2001 From: Ian Hou <45278651+iankhou@users.noreply.github.com> Date: Thu, 30 Jul 2026 17:29:22 -0400 Subject: [PATCH 15/34] fix(cli-integ): request 4-hour OIDC session for Windows integ jobs Windows cli shards run 30-80 minutes; tests still executing past the 1-hour default session expiry fail with '403 the security token included in the request is expired', including Atmosphere release calls. Request a 4-hour session on Windows jobs, matching the non-Atmosphere path. Takes effect only if the OIDC role's maximum session duration allows it, which this change also verifies. --- projenrc/cdk-cli-integ-tests.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/projenrc/cdk-cli-integ-tests.ts b/projenrc/cdk-cli-integ-tests.ts index 9e138a507..3af5485f1 100644 --- a/projenrc/cdk-cli-integ-tests.ts +++ b/projenrc/cdk-cli-integ-tests.ts @@ -53,14 +53,14 @@ function setupNodeStep(nodeVersion: string): github.workflows.JobStep { }; } -function awsAuthStep(props: CdkCliIntegTestsWorkflowProps, sessionName: string): github.workflows.JobStep { +function awsAuthStep(props: CdkCliIntegTestsWorkflowProps, sessionName: string, durationSeconds?: number): github.workflows.JobStep { return { name: 'Authenticate Via OIDC Role', id: 'creds', uses: 'aws-actions/configure-aws-credentials@v6', with: { 'aws-region': 'us-east-1', - 'role-duration-seconds': props.enableAtmosphere ? 60 * 60 : 4 * 60 * 60, + 'role-duration-seconds': durationSeconds ?? (props.enableAtmosphere ? 60 * 60 : 4 * 60 * 60), // Expect this in Environment Variables 'role-to-assume': props.enableAtmosphere ? props.enableAtmosphere.oidcRoleArn : '${{ vars.AWS_ROLE_TO_ASSUME_FOR_TESTING }}', 'role-session-name': sessionName, @@ -700,7 +700,10 @@ export class CdkCliIntegTestsWorkflow extends Component { // Run AWS OIDC auth after everything else, so creds are only easily accessible then. // This is defense in depth. Since the workflow's ambient identify is trusted, any script at any point can assume the OIDC role. // The OIDC role is designed to not be able to do anything else but vending Atmosphere creds. - awsAuthStep(this.props, 'run-tests@aws-cdk-cli-integ'), + // Windows jobs run considerably longer than the 1-hour default session; + // tests still running past expiry fail with 403s. Request a longer + // session there (subject to the role's maximum session duration). + awsAuthStep(this.props, 'run-tests@aws-cdk-cli-integ', platform.windows ? 4 * 60 * 60 : undefined), { name: 'Run the test suite: ${{ matrix.suite }}', run: `npx run-suite${this.maxWorkersArg}${shardArg} --use-cli-release=\${{ steps.versions.outputs.cli_version }} --framework-version=\${{ steps.versions.outputs.lib_version }} \${{ matrix.suite }}`, From 430217eee3acb7b25dae0a4a0348eef8d5250691 Mon Sep 17 00:00:00 2001 From: Ian Hou <45278651+iankhou@users.noreply.github.com> Date: Thu, 30 Jul 2026 17:49:49 -0400 Subject: [PATCH 16/34] revert(cli-integ): back to 1-hour OIDC session for Windows integ jobs The Atmosphere OIDC role's MaxSessionDuration is 1 hour: requesting 4 hours fails the assume-role call itself ('The requested DurationSeconds exceeds the MaxSessionDuration set for this role'). Revert to the 1-hour request and document why. Long-running Windows jobs will still 403 on tests running past expiry; fixing that needs a MaxSessionDuration bump on the role or credential refresh in the Atmosphere client. --- projenrc/cdk-cli-integ-tests.ts | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/projenrc/cdk-cli-integ-tests.ts b/projenrc/cdk-cli-integ-tests.ts index 3af5485f1..826017586 100644 --- a/projenrc/cdk-cli-integ-tests.ts +++ b/projenrc/cdk-cli-integ-tests.ts @@ -53,14 +53,19 @@ function setupNodeStep(nodeVersion: string): github.workflows.JobStep { }; } -function awsAuthStep(props: CdkCliIntegTestsWorkflowProps, sessionName: string, durationSeconds?: number): github.workflows.JobStep { +function awsAuthStep(props: CdkCliIntegTestsWorkflowProps, sessionName: string): github.workflows.JobStep { return { name: 'Authenticate Via OIDC Role', id: 'creds', uses: 'aws-actions/configure-aws-credentials@v6', with: { 'aws-region': 'us-east-1', - 'role-duration-seconds': durationSeconds ?? (props.enableAtmosphere ? 60 * 60 : 4 * 60 * 60), + // The Atmosphere OIDC role's MaxSessionDuration is 1 hour; requesting more + // fails the assume-role call outright. Tests running past expiry in + // long-running (Windows) jobs will fail with 403s; the fix has to come + // from a longer MaxSessionDuration on the role or credential refresh in + // the Atmosphere client, not from this request. + 'role-duration-seconds': props.enableAtmosphere ? 60 * 60 : 4 * 60 * 60, // Expect this in Environment Variables 'role-to-assume': props.enableAtmosphere ? props.enableAtmosphere.oidcRoleArn : '${{ vars.AWS_ROLE_TO_ASSUME_FOR_TESTING }}', 'role-session-name': sessionName, @@ -700,10 +705,7 @@ export class CdkCliIntegTestsWorkflow extends Component { // Run AWS OIDC auth after everything else, so creds are only easily accessible then. // This is defense in depth. Since the workflow's ambient identify is trusted, any script at any point can assume the OIDC role. // The OIDC role is designed to not be able to do anything else but vending Atmosphere creds. - // Windows jobs run considerably longer than the 1-hour default session; - // tests still running past expiry fail with 403s. Request a longer - // session there (subject to the role's maximum session duration). - awsAuthStep(this.props, 'run-tests@aws-cdk-cli-integ', platform.windows ? 4 * 60 * 60 : undefined), + awsAuthStep(this.props, 'run-tests@aws-cdk-cli-integ'), { name: 'Run the test suite: ${{ matrix.suite }}', run: `npx run-suite${this.maxWorkersArg}${shardArg} --use-cli-release=\${{ steps.versions.outputs.cli_version }} --framework-version=\${{ steps.versions.outputs.lib_version }} \${{ matrix.suite }}`, From 2d37e7052bd639e58b23ea212308a2442a13c8af Mon Sep 17 00:00:00 2001 From: Ian Hou <45278651+iankhou@users.noreply.github.com> Date: Thu, 30 Jul 2026 17:54:22 -0400 Subject: [PATCH 17/34] chore(cli-integ): remove session duration comment --- projenrc/cdk-cli-integ-tests.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/projenrc/cdk-cli-integ-tests.ts b/projenrc/cdk-cli-integ-tests.ts index 826017586..9e138a507 100644 --- a/projenrc/cdk-cli-integ-tests.ts +++ b/projenrc/cdk-cli-integ-tests.ts @@ -60,11 +60,6 @@ function awsAuthStep(props: CdkCliIntegTestsWorkflowProps, sessionName: string): uses: 'aws-actions/configure-aws-credentials@v6', with: { 'aws-region': 'us-east-1', - // The Atmosphere OIDC role's MaxSessionDuration is 1 hour; requesting more - // fails the assume-role call outright. Tests running past expiry in - // long-running (Windows) jobs will fail with 403s; the fix has to come - // from a longer MaxSessionDuration on the role or credential refresh in - // the Atmosphere client, not from this request. 'role-duration-seconds': props.enableAtmosphere ? 60 * 60 : 4 * 60 * 60, // Expect this in Environment Variables 'role-to-assume': props.enableAtmosphere ? props.enableAtmosphere.oidcRoleArn : '${{ vars.AWS_ROLE_TO_ASSUME_FOR_TESTING }}', From 6a88a4a9b47e328e43aa15100ee641387eb1bbdf Mon Sep 17 00:00:00 2001 From: Ian Hou <45278651+iankhou@users.noreply.github.com> Date: Thu, 30 Jul 2026 23:51:16 +0000 Subject: [PATCH 18/34] fix(cli-integ): spawn TTY processes through the shell on Windows node-pty's ConPTY backend resolves the target with SearchPath, which only finds real executables, not the .cmd shims npm generates for CLI entrypoints. Spawning 'cdk' this way fails with 'File not found:', breaking every test that needs a TTY (cdk destroy/import prompts, tty-app). Route the command through cmd.exe /c, mirroring what Process.spawn does with shell: true. --- packages/@aws-cdk-testing/cli-integ/lib/process.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/packages/@aws-cdk-testing/cli-integ/lib/process.ts b/packages/@aws-cdk-testing/cli-integ/lib/process.ts index 9b64ee585..7a987d51a 100644 --- a/packages/@aws-cdk-testing/cli-integ/lib/process.ts +++ b/packages/@aws-cdk-testing/cli-integ/lib/process.ts @@ -48,11 +48,18 @@ export class Process { * Spawn a process with a TTY attached. */ public static spawnTTY(command: string, args: string[], options: pty.IPtyForkOptions | pty.IWindowsPtyForkOptions = {}): IProcess { - const process = pty.spawn(command, args, { + // ConPTY resolves the spawned file with SearchPath, which only finds real + // executables — not the .cmd shims npm creates for CLI entrypoints. Route + // the command through the shell, like Process.spawn does with 'shell: true'. + if (process.platform === 'win32') { + args = ['/c', command, ...args]; + command = process.env.ComSpec ?? 'cmd.exe'; + } + const ptyProcess = pty.spawn(command, args, { name: 'xterm-color', ...options, }); - return new PtyProcess(process); + return new PtyProcess(ptyProcess); } /** From 77e1b5931b49499154bba06dfc8de59c0e617011 Mon Sep 17 00:00:00 2001 From: Ian Hou <45278651+iankhou@users.noreply.github.com> Date: Thu, 30 Jul 2026 23:51:25 +0000 Subject: [PATCH 19/34] fix(cli-integ): make cdk watch tests work on Windows The watch tests spawned 'cdk' directly with child_process.spawn, which cannot start npm .cmd shims on Windows ('spawn cdk ENOENT'); the tests then sat in waitForOutput until the 120s poll timeout. Spawn through a shell on win32 (new spawnWatch helper), kill the resulting process tree with taskkill so the watcher does not outlive the test, and replace the POSIX-only 'touch' with fs.utimesSync. --- ...es-with-directory-scoped-glob.integtest.ts | 6 ++--- ...s-with-glob-patterns-negative.integtest.ts | 6 ++--- ...le-changes-with-glob-patterns.integtest.ts | 9 +++---- .../cli-integ-tests/watch/watch-helpers.ts | 25 +++++++++++++++++-- 4 files changed, 31 insertions(+), 15 deletions(-) diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/watch/cdk-watch-detects-file-changes-with-directory-scoped-glob.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/watch/cdk-watch-detects-file-changes-with-directory-scoped-glob.integtest.ts index 62f2d6303..a3d226ede 100644 --- a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/watch/cdk-watch-detects-file-changes-with-directory-scoped-glob.integtest.ts +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/watch/cdk-watch-detects-file-changes-with-directory-scoped-glob.integtest.ts @@ -1,7 +1,6 @@ -import * as child_process from 'child_process'; import * as fs from 'fs'; import * as path from 'path'; -import { waitForOutput, waitForCondition, safeKillProcess } from './watch-helpers'; +import { waitForOutput, waitForCondition, safeKillProcess, spawnWatch } from './watch-helpers'; import { integTest, withDefaultFixture } from '../../../lib'; jest.setTimeout(5 * 60 * 1000); // 5 minutes for watch tests @@ -34,11 +33,10 @@ integTest( let output = ''; // Start cdk watch - const watchProcess = child_process.spawn('cdk', [ + const watchProcess = spawnWatch([ 'watch', '--hotswap', '-v', fixture.fullStackName('test-1'), ], { cwd: fixture.integTestDir, - stdio: 'pipe', env: { ...process.env, ...fixture.cdkShellEnv() }, }); diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/watch/cdk-watch-detects-file-changes-with-glob-patterns-negative.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/watch/cdk-watch-detects-file-changes-with-glob-patterns-negative.integtest.ts index 1dcab2a1a..a95f23482 100644 --- a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/watch/cdk-watch-detects-file-changes-with-glob-patterns-negative.integtest.ts +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/watch/cdk-watch-detects-file-changes-with-glob-patterns-negative.integtest.ts @@ -1,7 +1,6 @@ -import * as child_process from 'child_process'; import * as fs from 'fs'; import * as path from 'path'; -import { waitForOutput, safeKillProcess } from './watch-helpers'; +import { waitForOutput, safeKillProcess, spawnWatch } from './watch-helpers'; import { integTest, withDefaultFixture, sleep } from '../../../lib'; jest.setTimeout(5 * 60 * 1000); // 5 minutes for watch tests @@ -27,11 +26,10 @@ integTest( let output = ''; // Start cdk watch - const watchProcess = child_process.spawn('cdk', [ + const watchProcess = spawnWatch([ 'watch', '--hotswap', '-v', fixture.fullStackName('test-1'), ], { cwd: fixture.integTestDir, - stdio: 'pipe', env: { ...process.env, ...fixture.cdkShellEnv() }, }); diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/watch/cdk-watch-detects-file-changes-with-glob-patterns.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/watch/cdk-watch-detects-file-changes-with-glob-patterns.integtest.ts index 815a595fa..7b5f92bb8 100644 --- a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/watch/cdk-watch-detects-file-changes-with-glob-patterns.integtest.ts +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/watch/cdk-watch-detects-file-changes-with-glob-patterns.integtest.ts @@ -1,7 +1,6 @@ -import * as child_process from 'child_process'; import * as fs from 'fs'; import * as path from 'path'; -import { waitForOutput, waitForCondition, safeKillProcess } from './watch-helpers'; +import { waitForOutput, waitForCondition, safeKillProcess, spawnWatch } from './watch-helpers'; import { integTest, withDefaultFixture } from '../../../lib'; jest.setTimeout(5 * 60 * 1000); // 5 minutes for watch tests @@ -26,11 +25,10 @@ integTest( let output = ''; // Start cdk watch - const watchProcess = child_process.spawn('cdk', [ + const watchProcess = spawnWatch([ 'watch', '--hotswap', '-v', fixture.fullStackName('test-1'), ], { cwd: fixture.integTestDir, - stdio: 'pipe', env: { ...process.env, ...fixture.cdkShellEnv() }, }); @@ -51,7 +49,8 @@ integTest( fixture.log('✓ Initial deployment completed'); // Update the test file timestamp to trigger a watch event - child_process.spawnSync('touch', [testFile]); + const now = new Date(); + fs.utimesSync(testFile, now, now); await waitForOutput(() => output, 'Detected change to'); fixture.log('✓ Watch detected file change'); diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/watch/watch-helpers.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/watch/watch-helpers.ts index bbe2a918d..ca983fb96 100644 --- a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/watch/watch-helpers.ts +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/watch/watch-helpers.ts @@ -1,4 +1,5 @@ -import type { ChildProcess } from 'node:child_process'; +import * as child_process from 'node:child_process'; +import type { ChildProcess, SpawnOptions } from 'node:child_process'; const DEFAULT_POLL_TIMEOUT = 120_000; // 2 minutes @@ -33,12 +34,32 @@ export async function waitForCondition(condition: () => boolean): Promise expect(condition()).toBe(true); } +/** + * Spawn a long-running `cdk watch` process. + * + * On Windows the CLI is an npm .cmd shim, which `spawn` can only start + * through a shell ('spawn cdk ENOENT' otherwise). + */ +export function spawnWatch(args: string[], options: SpawnOptions): ChildProcess { + return child_process.spawn('cdk', args, { + stdio: 'pipe', + shell: process.platform === 'win32', + ...options, + }); +} + /** * Kill a spawned process. */ export function safeKillProcess(proc: ChildProcess): void { try { - proc.kill('SIGKILL'); + if (process.platform === 'win32' && proc.pid !== undefined) { + // Kill the whole tree: the process was spawned through a shell, + // so proc.pid is the shell and 'cdk watch' is its child. + child_process.spawnSync('taskkill', ['/pid', proc.pid.toString(), '/T', '/F']); + } else { + proc.kill('SIGKILL'); + } } catch { // process may have already exited } From 8d9615707fa1a684a3080fcd28dbeb2f7c390dbc Mon Sep 17 00:00:00 2001 From: Ian Hou <45278651+iankhou@users.noreply.github.com> Date: Thu, 30 Jul 2026 23:51:37 +0000 Subject: [PATCH 20/34] fix(cli-integ): deliver Windows skip list via file, add two docker tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two more tests build Linux docker images as a side effect and cannot pass on Windows-containers runners: 'test resource import with construct that requires bundling' (INCLUDE_NODEJS_FUNCTION_LAMBDA forces docker bundling of a NodejsFunction) and 'hotswap deployment supports Bedrock AgentCore Runtime' (builds a linux/arm64 DockerImageAsset). The session-tags skip name contains commas, which the comma-separated CDK_INTEG_SKIP_TESTS env var splits into fragments that match nothing — the test ran (and failed) despite being listed. Write the skip list to a newline-separated file and point CDK_INTEG_SKIP_TESTS_FILE at it instead. --- projenrc/cdk-cli-integ-tests.ts | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/projenrc/cdk-cli-integ-tests.ts b/projenrc/cdk-cli-integ-tests.ts index 9e138a507..2ae01749d 100644 --- a/projenrc/cdk-cli-integ-tests.ts +++ b/projenrc/cdk-cli-integ-tests.ts @@ -40,6 +40,8 @@ const DOCKER_TESTS_SKIPPED_ON_WINDOWS = [ 'CDK synth add the metadata properties expected by sam', 'can deploy with session tags on the deploy, lookup, file asset, and image asset publishing roles', 'generating and loading assembly', + 'test resource import with construct that requires bundling', + 'hotswap deployment supports Bedrock AgentCore Runtime', ]; function setupNodeStep(nodeVersion: string): github.workflows.JobStep { @@ -631,7 +633,10 @@ export class CdkCliIntegTestsWorkflow extends Component { IS_CANARY: 'true', CI: 'true', ...platform.windows ? { - CDK_INTEG_SKIP_TESTS: DOCKER_TESTS_SKIPPED_ON_WINDOWS.join(','), + // The skip file is newline-separated; the CDK_INTEG_SKIP_TESTS + // environment variable is comma-separated and cannot express + // test names that contain commas. + CDK_INTEG_SKIP_TESTS_FILE: '${{ github.workspace }}\\windows-skip-tests.txt', } : {}, // add extra env at end so it can override ...props.extraEnv, @@ -671,6 +676,13 @@ export class CdkCliIntegTestsWorkflow extends Component { // reads and writes the cache, so move it onto the Dev Drive too 'echo "npm_config_cache=${drive}:\\npm-cache" >> $env:GITHUB_ENV', ].join('\n'), + }, { + name: 'Write Windows skip-tests file', + run: [ + 'cat > windows-skip-tests.txt << \'EOF\'', + ...DOCKER_TESTS_SKIPPED_ON_WINDOWS, + 'EOF', + ].join('\n'), }] : [], github.WorkflowSteps.downloadArtifact({ with: { From 82bc7350140b4a93a847168e9bf689a3c9e06bc0 Mon Sep 17 00:00:00 2001 From: Ian Hou <45278651+iankhou@users.noreply.github.com> Date: Fri, 31 Jul 2026 06:55:43 +0000 Subject: [PATCH 21/34] fix(cli-integ): share one npm install across tests on Windows Every test installs aws-cdk-lib into its own temp directory. Writing out that package's tens of thousands of files takes ~3s on Linux but 10-12 minutes on Windows, and all ~15 jest workers doing it concurrently is the bulk of the Windows wall-clock (the same install takes 41s when a single worker runs it alone). On win32, install each distinct package set once per machine into a shared directory (workers coordinate through an atomic mkdir lock) and junction it into the test directory. rimraf now removes links without recursing into them so test cleanup cannot delete the shared install. --- .../@aws-cdk-testing/cli-integ/lib/shell.ts | 17 ++++- .../cli-integ/lib/with-cdk-app.ts | 72 ++++++++++++++++++- 2 files changed, 86 insertions(+), 3 deletions(-) diff --git a/packages/@aws-cdk-testing/cli-integ/lib/shell.ts b/packages/@aws-cdk-testing/cli-integ/lib/shell.ts index 1be3c53f4..73c5eb4e6 100644 --- a/packages/@aws-cdk-testing/cli-integ/lib/shell.ts +++ b/packages/@aws-cdk-testing/cli-integ/lib/shell.ts @@ -282,7 +282,22 @@ export class ShellHelper { export function rimraf(fsPath: string): boolean { try { let success = true; - const isDir = fs.lstatSync(fsPath).isDirectory(); + const stat = fs.lstatSync(fsPath); + + // Remove links without recursing into their target: a directory may + // link to shared content that other tests are still using (e.g. the + // shared 'node_modules' on Windows). + if (stat.isSymbolicLink()) { + try { + fs.unlinkSync(fsPath); + } catch { + // On Windows, directory links (junctions) must be removed with rmdir + fs.rmdirSync(fsPath); + } + return true; + } + + const isDir = stat.isDirectory(); if (isDir) { for (const file of fs.readdirSync(fsPath)) { diff --git a/packages/@aws-cdk-testing/cli-integ/lib/with-cdk-app.ts b/packages/@aws-cdk-testing/cli-integ/lib/with-cdk-app.ts index bddfc3650..4f46dda3c 100644 --- a/packages/@aws-cdk-testing/cli-integ/lib/with-cdk-app.ts +++ b/packages/@aws-cdk-testing/cli-integ/lib/with-cdk-app.ts @@ -1,5 +1,6 @@ /* eslint-disable no-console */ import assert from 'assert'; +import * as crypto from 'crypto'; import * as fs from 'fs'; import * as os from 'os'; import * as path from 'path'; @@ -13,7 +14,7 @@ import type { ITestCliSource, ITestLibrarySource } from './package-sources/sourc import { testSource } from './package-sources/subprocess'; import { RESOURCES_DIR } from './resources'; import type { ShellOptions } from './shell'; -import { ShellHelper, rimraf } from './shell'; +import { shell, ShellHelper, rimraf } from './shell'; import type { AwsContext, AwsContextOptions } from './with-aws'; import { atmosphereEnabled, withAws } from './with-aws'; import { withTimeout } from './with-timeout'; @@ -1064,6 +1065,70 @@ export async function installNpmPackages(fixture: TestFixture, packages: Record< devDependencies: packages, }, undefined, 2), { encoding: 'utf-8' }); + if (process.platform === 'win32') { + // Installing aws-cdk-lib means writing out tens of thousands of small + // files, which is very slow on Windows (minutes instead of seconds), + // and every concurrent jest worker doing so at once makes it slower + // still. Install every distinct package set only once per machine and + // junction it into the test directory. + const sharedNodeModules = await sharedPackageSetInstall(fixture, packages); + fs.symlinkSync(sharedNodeModules, path.join(fixture.integTestDir, 'node_modules'), 'junction'); + return; + } + + await npmInstallWithRetry(fixture, fixture.integTestDir); +} + +/** + * Install the given package set into a machine-shared directory, once. + * + * Concurrent callers (jest workers are separate processes) coordinate via an + * atomically-created lock directory; whoever wins installs while the rest + * poll for the completion marker. + * + * @returns the path of the installed `node_modules` directory. + */ +async function sharedPackageSetInstall(fixture: TestFixture, packages: Record): Promise { + const hash = crypto.createHash('sha256').update(JSON.stringify(packages)).digest('hex').slice(0, 16); + const sharedDir = path.join(os.tmpdir(), `cdk-integ-shared-${hash}`); + const nodeModules = path.join(sharedDir, 'node_modules'); + const completeMarker = path.join(sharedDir, '.install-complete'); + const lockDir = `${sharedDir}.lock`; + + const deadline = Date.now() + 30 * 60 * 1000; + while (true) { + if (fs.existsSync(completeMarker)) { + return nodeModules; + } + if (Date.now() > deadline) { + throw new Error(`Timed out waiting for shared install of ${JSON.stringify(packages)} in '${sharedDir}'`); + } + + try { + fs.mkdirSync(lockDir); + } catch { + // Another worker is installing; wait for it to finish. + await sleep(5_000); + continue; + } + + try { + if (fs.existsSync(completeMarker)) { + return nodeModules; + } + fixture.log(`Installing shared package set into '${sharedDir}'`); + fs.mkdirSync(sharedDir, { recursive: true }); + fs.copyFileSync(path.join(fixture.integTestDir, 'package.json'), path.join(sharedDir, 'package.json')); + await npmInstallWithRetry(fixture, sharedDir); + fs.writeFileSync(completeMarker, ''); + return nodeModules; + } finally { + fs.rmdirSync(lockDir); + } + } +} + +async function npmInstallWithRetry(fixture: TestFixture, cwd: string) { // we often ECONNRESET from NPM so lets retry. this might be because of high concurrency // which overwhelmes system resources. const timeoutMinutes = 10; @@ -1073,7 +1138,10 @@ export async function installNpmPackages(fixture: TestFixture, packages: Record< while (true) { try { // Now install that `package.json` using NPM7 - await fixture.shell(['node', require.resolve('npm'), 'install']); + await shell(['node', require.resolve('npm'), 'install'], { + cwd, + outputs: [fixture.output], + }); break; } catch (e: any) { if (Date.now() < timeoutDate.getTime() && fixture.output.toString().includes('ECONNRESET' )) { From 1036149d4ccb97b7012a30635acff866ebd8f10d Mon Sep 17 00:00:00 2001 From: Ian Hou <45278651+iankhou@users.noreply.github.com> Date: Fri, 31 Jul 2026 14:50:29 +0000 Subject: [PATCH 22/34] fix(cli-integ): widen ConPTY terminal so long prompts match 'cdk import prompts the user for sns topic arns' hangs on Windows: the prompt line (resource path + ARN hint) is longer than the default 80 columns, and ConPTY hard-wraps output at the terminal width, so the line-based prompt regex never matches and the test waits until the 20-minute timeout. Use 512 columns so no real prompt ever wraps. --- packages/@aws-cdk-testing/cli-integ/lib/process.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/@aws-cdk-testing/cli-integ/lib/process.ts b/packages/@aws-cdk-testing/cli-integ/lib/process.ts index 7a987d51a..5e08f966e 100644 --- a/packages/@aws-cdk-testing/cli-integ/lib/process.ts +++ b/packages/@aws-cdk-testing/cli-integ/lib/process.ts @@ -57,6 +57,11 @@ export class Process { } const ptyProcess = pty.spawn(command, args, { name: 'xterm-color', + // Wide enough that no output line ever hits the terminal width: ConPTY + // (unlike Unix ptys) renders the screen buffer and inserts hard line + // breaks at the width, which splits long prompts across lines and + // breaks the line-based prompt matching in shell(). + cols: 512, ...options, }); return new PtyProcess(ptyProcess); From 136382308927598ad64f18275c1bf510604d836f Mon Sep 17 00:00:00 2001 From: Ian Hou <45278651+iankhou@users.noreply.github.com> Date: Fri, 31 Jul 2026 14:50:29 +0000 Subject: [PATCH 23/34] fix(cli-integ): skip sam local test on Windows Synthesizing the SAM fixture app bundles a python lambda with aws-lambda-python-alpha, which builds the Linux image public.ecr.aws/sam/build-python3.12 - impossible in Windows-containers mode. Skip list is now 20. --- projenrc/cdk-cli-integ-tests.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/projenrc/cdk-cli-integ-tests.ts b/projenrc/cdk-cli-integ-tests.ts index 2ae01749d..ae944e801 100644 --- a/projenrc/cdk-cli-integ-tests.ts +++ b/projenrc/cdk-cli-integ-tests.ts @@ -42,6 +42,7 @@ const DOCKER_TESTS_SKIPPED_ON_WINDOWS = [ 'generating and loading assembly', 'test resource import with construct that requires bundling', 'hotswap deployment supports Bedrock AgentCore Runtime', + 'sam can locally test the synthesized cdk application', ]; function setupNodeStep(nodeVersion: string): github.workflows.JobStep { From b339d3ee12722d81cd15e7c0d3f530fc640393a4 Mon Sep 17 00:00:00 2001 From: Ian Hou <45278651+iankhou@users.noreply.github.com> Date: Fri, 31 Jul 2026 18:39:25 +0000 Subject: [PATCH 24/34] fix(cli-integ): match prompts in ConPTY screen-buffer output The second prompt of 'cdk import prompts the user for sns topic arns' never matched on Windows: ConPTY draws it with an absolute cursor-positioning sequence, pads it with spaces to the terminal width, and follows it with lines containing only control sequences. The line-based matcher saw a control-only 'last line' and waited forever. On win32, strip ANSI escape sequences and match against the last line with visible content. --- .../@aws-cdk-testing/cli-integ/lib/shell.ts | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/packages/@aws-cdk-testing/cli-integ/lib/shell.ts b/packages/@aws-cdk-testing/cli-integ/lib/shell.ts index 73c5eb4e6..b1d2cee66 100644 --- a/packages/@aws-cdk-testing/cli-integ/lib/shell.ts +++ b/packages/@aws-cdk-testing/cli-integ/lib/shell.ts @@ -354,7 +354,28 @@ export function addToShellPath(x: string) { class LastLine { private lastLine: string = ''; + // win32 only: the last completed line that had visible content, see below + private lastVisibleLine: string = ''; + public append(chunk: string): void { + if (process.platform === 'win32') { + // ConPTY renders the screen buffer instead of streaming plain text: + // prompts are drawn with cursor-positioning escape sequences, padded + // with spaces to the terminal width, and followed by "lines" that + // contain nothing but more escape sequences. Match against the last + // line that had visible content, so control-only lines don't erase a + // prompt that was just drawn. + const lines = stripAnsi(chunk).split(/\r?\n/); + this.lastLine += lines[0]; + for (const line of lines.slice(1)) { + if (this.lastLine.trim().length > 0) { + this.lastVisibleLine = this.lastLine; + } + this.lastLine = line; + } + return; + } + const lines = chunk.split(os.EOL); if (lines.length === 1) { // chunk doesn't contain a new line so just append @@ -366,10 +387,30 @@ class LastLine { } public get(): string { + if (process.platform === 'win32' && this.lastLine.trim().length === 0) { + return this.lastVisibleLine; + } return this.lastLine; } public reset() { this.lastLine = ''; + this.lastVisibleLine = ''; } } + +const ESC = '\u001b'; +// CSI sequences (cursor movement, erase, colors) and OSC sequences (window title) +const ANSI_REGEX = new RegExp(`${ESC}\\[[0-9;?]*[@-~]|${ESC}\\][^${ESC}\\u0007]*(?:\\u0007|${ESC}\\\\)`, 'g'); + +/** + * Remove ANSI escape sequences from terminal output. + * + * Windows ConPTY renders the screen buffer rather than streaming plain text: + * once the cursor reaches the bottom of the buffer, lines arrive as absolute + * cursor-positioning sequences instead of newline-terminated text. Prompt + * matching must look at the text only. + */ +function stripAnsi(chunk: string): string { + return chunk.replace(ANSI_REGEX, ''); +} From 92ae5580980c44067e00feb8d91ea1b7f47db450 Mon Sep 17 00:00:00 2001 From: Ian Hou <45278651+iankhou@users.noreply.github.com> Date: Sat, 1 Aug 2026 00:55:32 +0000 Subject: [PATCH 25/34] fix(cli-integ): start Verdaccio without pm2, poll for readiness Every integ job installs Verdaccio globally before running tests. pm2 roughly doubled that install (77 of 398 packages) and only served to daemonize a process that just needs to outlive the job step; a nohup'd node process does the same. Replace the fixed 'sleep 5' with a poll of the registry endpoint (typically ready in 1-2s), and quiet the install with --no-audit --no-fund. Saves roughly 60-90s on Windows jobs and ~10s on Linux jobs, per job. --- projenrc/cdk-cli-integ-tests.ts | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/projenrc/cdk-cli-integ-tests.ts b/projenrc/cdk-cli-integ-tests.ts index ae944e801..5c3a08343 100644 --- a/projenrc/cdk-cli-integ-tests.ts +++ b/projenrc/cdk-cli-integ-tests.ts @@ -333,16 +333,22 @@ export class CdkCliIntegTestsWorkflow extends Component { committed: false, lines: [ '#!/bin/bash', - 'npm install -g verdaccio pm2', + // No process manager: Verdaccio only has to outlive this job, and the + // runner kills leftover processes at job teardown. A plain detached + // spawn avoids installing pm2, which used to double the install time. + 'npm install -g --no-audit --no-fund --loglevel=error verdaccio', 'mkdir -p $HOME/.config/verdaccio', `echo '${JSON.stringify(verdaccioConfig)}' > $HOME/.config/verdaccio/config.yaml`, - // Start Verdaccio through pm2 by pointing at its JS entrypoint with an - // explicit Node interpreter. On Windows the global `verdaccio` bin is a - // `.cmd` shim, which pm2's fork mode cannot execute; the resolved JS - // file works on every platform. + // Point at Verdaccio's JS entrypoint: on Windows the global bin is a + // `.cmd` shim that cannot be spawned from bash directly. 'VERDACCIO_BIN="$(npm root -g)/verdaccio/bin/verdaccio"', - 'pm2 start "$VERDACCIO_BIN" --interpreter node -- --config $HOME/.config/verdaccio/config.yaml', - 'sleep 5', // Wait for Verdaccio to start + 'nohup node "$VERDACCIO_BIN" --config $HOME/.config/verdaccio/config.yaml > verdaccio.log 2>&1 &', + // Wait for Verdaccio to accept requests instead of sleeping a fixed time + 'for i in $(seq 1 60); do', + ' if curl -fsS -o /dev/null http://localhost:4873/; then break; fi', + ' if [ $i -eq 60 ]; then echo "Verdaccio did not start:"; cat verdaccio.log; exit 1; fi', + ' sleep 1', + 'done', // Configure NPM to use local registry 'echo \'//localhost:4873/:_authToken="MWRjNDU3OTE1NTljYWUyOTFkMWJkOGUyYTIwZWMwNTI6YTgwZjkyNDE0NzgwYWQzNQ=="\' > ~/.npmrc', 'echo \'registry=http://localhost:4873/\' >> ~/.npmrc', From 76eccbe8da4b10bc79ed841719798d44000d6d80 Mon Sep 17 00:00:00 2001 From: Ian Hou <45278651+iankhou@users.noreply.github.com> Date: Sat, 1 Aug 2026 01:57:15 +0000 Subject: [PATCH 26/34] feat(cli-integ): share a weekly npm cache across integ jobs Every integ job downloads the same packages from scratch: the Verdaccio global install, @aws-cdk-testing/cli-integ and its large dependency tree (Jest, 14 AWS SDK clients, npm itself), and aws-cdk-lib per test. Cache the npm cache directory (the Dev Drive path on Windows, ~/.npm on Linux) with a weekly rotating key shared per platform. Locally published '.999' candidates change content under an unchanging version number, so jobs set npm_config_prefer_online: metadata is always revalidated against the registry and only integrity-matching tarballs are served from the cache. The npm cache is content-addressed, so a stale tarball can never satisfy a new integrity hash. --- projenrc/cdk-cli-integ-tests.ts | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/projenrc/cdk-cli-integ-tests.ts b/projenrc/cdk-cli-integ-tests.ts index 5c3a08343..a79f45b1b 100644 --- a/projenrc/cdk-cli-integ-tests.ts +++ b/projenrc/cdk-cli-integ-tests.ts @@ -639,6 +639,12 @@ export class CdkCliIntegTestsWorkflow extends Component { // assumptions about the availability of source packages. IS_CANARY: 'true', CI: 'true', + // The npm cache is shared across runs (see the cache step below), but + // the locally published '.999' candidate packages change content while + // keeping the same version number. Always revalidate metadata against + // the registry (it is localhost, so this is cheap); tarballs that + // still match their integrity hash are served from the cache. + npm_config_prefer_online: 'true', ...platform.windows ? { // The skip file is newline-separated; the CDK_INTEG_SKIP_TESTS // environment variable is comma-separated and cannot express @@ -691,6 +697,30 @@ export class CdkCliIntegTestsWorkflow extends Component { 'EOF', ].join('\n'), }] : [], + { + name: 'Compute cache key', + id: 'cachekey', + // Rotate the cache weekly so it follows dependency updates without + // growing unboundedly; correctness does not depend on freshness + // because npm revalidates metadata (npm_config_prefer_online). + run: 'echo "week=$(date +%G-%V)" >> $GITHUB_OUTPUT', + }, + { + // The npm cache makes the Verdaccio global install, the cli-integ + // install, and the per-test framework installs mostly network-free. + // All jobs of a platform share one weekly cache entry: the first + // job to finish on a cache miss saves it, concurrent saves of the + // same key are rejected and harmless. + name: 'Restore npm cache', + uses: 'actions/cache@v4', + with: { + // On Windows this is the Dev Drive path exported by the setup + // step above; on Linux it is npm's default cache location. + 'path': platform.windows ? '${{ env.npm_config_cache }}' : '~/.npm', + 'key': `npm-cache-${platform.windows ? 'windows' : 'linux'}-\${{ steps.cachekey.outputs.week }}`, + 'restore-keys': `npm-cache-${platform.windows ? 'windows' : 'linux'}-`, + }, + }, github.WorkflowSteps.downloadArtifact({ with: { artifactIds: [`\${{needs.${this.JOB_PREPARE}.outputs.packagesArtifact}}`], From 2fb8afbff5f4a8a2012d50ab12b8ce277a4cbf31 Mon Sep 17 00:00:00 2001 From: Ian Hou <45278651+iankhou@users.noreply.github.com> Date: Sat, 1 Aug 2026 04:39:08 +0000 Subject: [PATCH 27/34] Revert "feat(cli-integ): share a weekly npm cache across integ jobs" This reverts commit 2e0a1f1979e029e886e037d05ad84157f918377d. --- projenrc/cdk-cli-integ-tests.ts | 30 ------------------------------ 1 file changed, 30 deletions(-) diff --git a/projenrc/cdk-cli-integ-tests.ts b/projenrc/cdk-cli-integ-tests.ts index a79f45b1b..5c3a08343 100644 --- a/projenrc/cdk-cli-integ-tests.ts +++ b/projenrc/cdk-cli-integ-tests.ts @@ -639,12 +639,6 @@ export class CdkCliIntegTestsWorkflow extends Component { // assumptions about the availability of source packages. IS_CANARY: 'true', CI: 'true', - // The npm cache is shared across runs (see the cache step below), but - // the locally published '.999' candidate packages change content while - // keeping the same version number. Always revalidate metadata against - // the registry (it is localhost, so this is cheap); tarballs that - // still match their integrity hash are served from the cache. - npm_config_prefer_online: 'true', ...platform.windows ? { // The skip file is newline-separated; the CDK_INTEG_SKIP_TESTS // environment variable is comma-separated and cannot express @@ -697,30 +691,6 @@ export class CdkCliIntegTestsWorkflow extends Component { 'EOF', ].join('\n'), }] : [], - { - name: 'Compute cache key', - id: 'cachekey', - // Rotate the cache weekly so it follows dependency updates without - // growing unboundedly; correctness does not depend on freshness - // because npm revalidates metadata (npm_config_prefer_online). - run: 'echo "week=$(date +%G-%V)" >> $GITHUB_OUTPUT', - }, - { - // The npm cache makes the Verdaccio global install, the cli-integ - // install, and the per-test framework installs mostly network-free. - // All jobs of a platform share one weekly cache entry: the first - // job to finish on a cache miss saves it, concurrent saves of the - // same key are rejected and harmless. - name: 'Restore npm cache', - uses: 'actions/cache@v4', - with: { - // On Windows this is the Dev Drive path exported by the setup - // step above; on Linux it is npm's default cache location. - 'path': platform.windows ? '${{ env.npm_config_cache }}' : '~/.npm', - 'key': `npm-cache-${platform.windows ? 'windows' : 'linux'}-\${{ steps.cachekey.outputs.week }}`, - 'restore-keys': `npm-cache-${platform.windows ? 'windows' : 'linux'}-`, - }, - }, github.WorkflowSteps.downloadArtifact({ with: { artifactIds: [`\${{needs.${this.JOB_PREPARE}.outputs.packagesArtifact}}`], From 85c2ec910c5053a714cdf07bb252970339581b30 Mon Sep 17 00:00:00 2001 From: Ian Hou <45278651+iankhou@users.noreply.github.com> Date: Sat, 1 Aug 2026 04:42:10 +0000 Subject: [PATCH 28/34] fix(cli-integ): ship Verdaccio to test jobs as a prebuilt bundle Installing Verdaccio through npm in every test job costs ~60s on Windows runners, dominated by writing thousands of small files - a warm npm cache does not help because extraction, not download, is the bottleneck. Install it once in the 'prepare' job instead and ship node_modules as a tarball in the script artifact (8.6MB); jobs extract a single archive in about a second. Verdaccio has no native or platform-specific dependencies, so the Linux-built tree runs on Windows; --no-bin-links keeps npm bin symlinks out of the archive (jobs invoke the JS entrypoint directly, symlinks would not survive the artifact round-trip on Windows anyway). --- .github/workflows/integ.yml | 9 +++++++- projenrc/cdk-cli-integ-tests.ts | 37 ++++++++++++++++++++++++++------- 2 files changed, 37 insertions(+), 9 deletions(-) diff --git a/.github/workflows/integ.yml b/.github/workflows/integ.yml index e6a059ff5..11d778844 100644 --- a/.github/workflows/integ.yml +++ b/.github/workflows/integ.yml @@ -74,6 +74,11 @@ jobs: env: RELEASE: "true" run: yarn projen build + - name: Bundle Verdaccio for the test jobs + run: |- + mkdir -p /tmp/verdaccio-bundle + (cd /tmp/verdaccio-bundle && npm install --no-bin-links --no-audit --no-fund --loglevel=error verdaccio) + tar czf .projen/verdaccio-bundle.tgz -C /tmp/verdaccio-bundle node_modules - name: Upload artifact id: build-artifact uses: actions/upload-artifact@v7 @@ -86,7 +91,9 @@ jobs: uses: actions/upload-artifact@v7 with: name: script-artifact - path: .projen/*.sh + path: |- + .projen/*.sh + .projen/verdaccio-bundle.tgz overwrite: true include-hidden-files: true integ_cli: diff --git a/projenrc/cdk-cli-integ-tests.ts b/projenrc/cdk-cli-integ-tests.ts index 5c3a08343..e43bf662b 100644 --- a/projenrc/cdk-cli-integ-tests.ts +++ b/projenrc/cdk-cli-integ-tests.ts @@ -333,15 +333,18 @@ export class CdkCliIntegTestsWorkflow extends Component { committed: false, lines: [ '#!/bin/bash', - // No process manager: Verdaccio only has to outlive this job, and the - // runner kills leftover processes at job teardown. A plain detached - // spawn avoids installing pm2, which used to double the install time. - 'npm install -g --no-audit --no-fund --loglevel=error verdaccio', + // Verdaccio was installed once in the 'prepare' job and shipped here + // as a tarball; extracting it is much faster than an npm install, + // especially on Windows. No process manager: Verdaccio only has to + // outlive this job, and the runner kills leftover processes at job + // teardown. + 'mkdir -p $HOME/verdaccio-app', + 'tar xzf .projen/verdaccio-bundle.tgz -C $HOME/verdaccio-app', 'mkdir -p $HOME/.config/verdaccio', `echo '${JSON.stringify(verdaccioConfig)}' > $HOME/.config/verdaccio/config.yaml`, - // Point at Verdaccio's JS entrypoint: on Windows the global bin is a - // `.cmd` shim that cannot be spawned from bash directly. - 'VERDACCIO_BIN="$(npm root -g)/verdaccio/bin/verdaccio"', + // Point at Verdaccio's JS entrypoint; bin shims were not created + // (--no-bin-links) and would not be bash-spawnable on Windows anyway. + 'VERDACCIO_BIN="$HOME/verdaccio-app/node_modules/verdaccio/bin/verdaccio"', 'nohup node "$VERDACCIO_BIN" --config $HOME/.config/verdaccio/config.yaml > verdaccio.log 2>&1 &', // Wait for Verdaccio to accept requests instead of sleeping a fixed time 'for i in $(seq 1 60); do', @@ -481,6 +484,21 @@ export class CdkCliIntegTestsWorkflow extends Component { RELEASE: 'true', }, }, + { + // Install Verdaccio once here and ship it to the test jobs as a + // tarball. Installing it in every job through npm costs ~60s on + // Windows runners (thousands of small file writes); extracting a + // single archive is much faster. Verdaccio has no native or + // platform-specific dependencies, so a Linux-built tree runs + // anywhere; --no-bin-links keeps symlinks out of the archive + // (jobs invoke the JS entrypoint directly). + name: 'Bundle Verdaccio for the test jobs', + run: [ + 'mkdir -p /tmp/verdaccio-bundle', + '(cd /tmp/verdaccio-bundle && npm install --no-bin-links --no-audit --no-fund --loglevel=error verdaccio)', + 'tar czf .projen/verdaccio-bundle.tgz -C /tmp/verdaccio-bundle node_modules', + ].join('\n'), + }, github.WorkflowSteps.uploadArtifact({ id: 'build-artifact', with: { @@ -493,7 +511,10 @@ export class CdkCliIntegTestsWorkflow extends Component { id: 'script-artifact', with: { name: 'script-artifact', - path: '.projen/*.sh', + path: [ + '.projen/*.sh', + '.projen/verdaccio-bundle.tgz', + ].join('\n'), overwrite: true, includeHiddenFiles: true, }, From 12a49830df133372beaffbcb9026b0a8dc335c8e Mon Sep 17 00:00:00 2001 From: Ian Hou <45278651+iankhou@users.noreply.github.com> Date: Sat, 1 Aug 2026 05:43:50 +0000 Subject: [PATCH 29/34] fix(cli-integ): pin bundled Verdaccio to 6.8 for Node 20 jobs The prebuilt bundle runs under every Node version in the test matrix. Verdaccio 6.9 requires Node >= 22 and refused to start on the Node 20 jobs; 6.8 supports Node >= 20. The previous per-job npm install masked this by resolving an engines-compatible version per job. --- .github/workflows/integ.yml | 2 +- projenrc/cdk-cli-integ-tests.ts | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/.github/workflows/integ.yml b/.github/workflows/integ.yml index 11d778844..66f5014e6 100644 --- a/.github/workflows/integ.yml +++ b/.github/workflows/integ.yml @@ -77,7 +77,7 @@ jobs: - name: Bundle Verdaccio for the test jobs run: |- mkdir -p /tmp/verdaccio-bundle - (cd /tmp/verdaccio-bundle && npm install --no-bin-links --no-audit --no-fund --loglevel=error verdaccio) + (cd /tmp/verdaccio-bundle && npm install --no-bin-links --no-audit --no-fund --loglevel=error verdaccio@6.8) tar czf .projen/verdaccio-bundle.tgz -C /tmp/verdaccio-bundle node_modules - name: Upload artifact id: build-artifact diff --git a/projenrc/cdk-cli-integ-tests.ts b/projenrc/cdk-cli-integ-tests.ts index e43bf662b..ea0ab75c0 100644 --- a/projenrc/cdk-cli-integ-tests.ts +++ b/projenrc/cdk-cli-integ-tests.ts @@ -495,7 +495,12 @@ export class CdkCliIntegTestsWorkflow extends Component { name: 'Bundle Verdaccio for the test jobs', run: [ 'mkdir -p /tmp/verdaccio-bundle', - '(cd /tmp/verdaccio-bundle && npm install --no-bin-links --no-audit --no-fund --loglevel=error verdaccio)', + // The bundle is built once but runs under every Node version in + // the test matrix, so Verdaccio's engine range must include the + // oldest of them: 6.9 requires Node >= 22, 6.8 still allows 20. + // (A per-job npm install used to hide this by resolving an + // engines-compatible version for each job's own Node.) + '(cd /tmp/verdaccio-bundle && npm install --no-bin-links --no-audit --no-fund --loglevel=error verdaccio@6.8)', 'tar czf .projen/verdaccio-bundle.tgz -C /tmp/verdaccio-bundle node_modules', ].join('\n'), }, From 360fc94b4e63d7cf0bd7e5d810eca87fd79fe203 Mon Sep 17 00:00:00 2001 From: dgandhi62 Date: Fri, 14 Aug 2026 10:16:51 -0400 Subject: [PATCH 30/34] fix(cli-integ): fall back to npm install when Verdaccio bundle is missing The pull_request_target trigger reads the workflow from main, which doesn't have the Bundle Verdaccio step yet. Add a conditional so the script installs Verdaccio via npm when the bundle is absent. --- projenrc/cdk-cli-integ-tests.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/projenrc/cdk-cli-integ-tests.ts b/projenrc/cdk-cli-integ-tests.ts index ea0ab75c0..59791914a 100644 --- a/projenrc/cdk-cli-integ-tests.ts +++ b/projenrc/cdk-cli-integ-tests.ts @@ -338,8 +338,16 @@ export class CdkCliIntegTestsWorkflow extends Component { // especially on Windows. No process manager: Verdaccio only has to // outlive this job, and the runner kills leftover processes at job // teardown. + // + // Fallback: if the tarball is not present (e.g. when pull_request_target + // uses the base branch workflow which lacks the bundle step), install + // Verdaccio on the fly. Slower, but keeps the run working. 'mkdir -p $HOME/verdaccio-app', - 'tar xzf .projen/verdaccio-bundle.tgz -C $HOME/verdaccio-app', + 'if [ -f .projen/verdaccio-bundle.tgz ]; then', + ' tar xzf .projen/verdaccio-bundle.tgz -C $HOME/verdaccio-app', + 'else', + ' npm install --prefix $HOME/verdaccio-app --no-bin-links --no-audit --no-fund --loglevel=error verdaccio@6.8', + 'fi', 'mkdir -p $HOME/.config/verdaccio', `echo '${JSON.stringify(verdaccioConfig)}' > $HOME/.config/verdaccio/config.yaml`, // Point at Verdaccio's JS entrypoint; bin shims were not created From 26fe26dfd68ca2f6c753dc744b00f6d0ba35a746 Mon Sep 17 00:00:00 2001 From: dgandhi62 Date: Tue, 18 Aug 2026 13:41:16 -0400 Subject: [PATCH 31/34] feat(cli-integ): run Windows integ suites nightly and on label, not every PR --- .github/workflows/integ.yml | 979 +++++++++++++++++++++++++++++++- projenrc/cdk-cli-integ-tests.ts | 213 ++++++- 2 files changed, 1148 insertions(+), 44 deletions(-) diff --git a/.github/workflows/integ.yml b/.github/workflows/integ.yml index 66f5014e6..8c7a7c11a 100644 --- a/.github/workflows/integ.yml +++ b/.github/workflows/integ.yml @@ -4,8 +4,16 @@ name: integ on: pull_request_target: branches: [] + types: + - opened + - synchronize + - reopened + - labeled + - unlabeled merge_group: {} workflow_dispatch: {} + schedule: + - cron: 0 6 * * * jobs: determine_env: runs-on: ubuntu-latest @@ -17,9 +25,9 @@ jobs: - name: Start requiring approval id: start_requiring_approval run: echo integ-approval > .envname - - name: Skip approval for mergeGroup or PR created from this repo - id: skip_approval_for_mergegroup_or_pr_created_from_this_repo - if: ${{ github.event_name == 'merge_group' || github.event.pull_request.head.repo.full_name == github.repository }} + - name: Skip approval for mergeGroup, schedule, or PR created from this repo + id: skip_approval_for_mergegroup_schedule_or_pr_created_from_this_repo + if: ${{ github.event_name == 'merge_group' || github.event_name == 'schedule' || github.event.pull_request.head.repo.full_name == github.repository }} run: echo no-approval > .envname - name: Output the value id: output @@ -44,8 +52,8 @@ jobs: id: checkout uses: actions/checkout@v7 with: - ref: ${{ github.event.pull_request.head.sha }} - repository: ${{ github.event.pull_request.head.repo.full_name }} + ref: ${{ github.event.pull_request.head.sha || github.sha }} + repository: ${{ github.event.pull_request.head.repo.full_name || github.repository }} allow-unsafe-pr-checkout: true - name: Fetch tags from origin repo id: fetch_tags_from_origin_repo @@ -75,6 +83,7 @@ jobs: RELEASE: "true" run: yarn projen build - name: Bundle Verdaccio for the test jobs + id: bundle_verdaccio_for_the_test_jobs run: |- mkdir -p /tmp/verdaccio-bundle (cd /tmp/verdaccio-bundle && npm install --no-bin-links --no-audit --no-fund --loglevel=error verdaccio@6.8) @@ -108,7 +117,528 @@ jobs: MAVEN_ARGS: --no-transfer-progress IS_CANARY: "true" CI: "true" - if: github.event_name != 'merge_group' && !contains(github.event.pull_request.labels.*.name, 'pr/exempt-integ-test') + defaults: + run: + shell: bash + if: github.event_name != 'merge_group' && !contains(github.event.pull_request.labels.*.name, 'pr/exempt-integ-test') && github.event_name != 'schedule' + steps: + - name: Download artifact + id: download_artifact + uses: actions/download-artifact@v8 + with: + artifact-ids: ${{needs.prepare.outputs.packagesArtifact}} + path: packages + - name: Download artifact + id: download_artifact_2 + uses: actions/download-artifact@v8 + with: + artifact-ids: ${{needs.prepare.outputs.scriptsArtifact}} + path: .projen + - name: Setup Node.js + id: setup_node_js + uses: actions/setup-node@v6 + with: + node-version: ${{ matrix.node }} + package-manager-cache: false + - name: Set up JDK 18 + id: set_up_jdk_18 + if: matrix.suite == 'init-java' || matrix.suite == 'cli-integ-tests' + uses: actions/setup-java@v5 + with: + java-version: "18" + distribution: corretto + - name: Set git identity + id: set_git_identity + run: |- + git config --global user.name "aws-cdk-cli-integ" + git config --global user.email "noreply@example.com" + - name: Prepare Verdaccio + id: prepare_verdaccio + run: chmod +x .projen/prepare-verdaccio.sh && .projen/prepare-verdaccio.sh + - name: Download and install the test artifact + id: download_and_install_the_test_artifact + run: npm install @aws-cdk-testing/cli-integ + - name: Determine latest package versions + id: versions + run: |- + CLI_VERSION=$(cd ${TMPDIR:-/tmp} && npm view aws-cdk version) + echo "CLI version: ${CLI_VERSION}" + echo "cli_version=${CLI_VERSION}" >> $GITHUB_OUTPUT + LIB_VERSION=$(cd ${TMPDIR:-/tmp} && npm view aws-cdk-lib version) + echo "lib version: ${LIB_VERSION}" + echo "lib_version=${LIB_VERSION}" >> $GITHUB_OUTPUT + - name: Authenticate Via OIDC Role + id: creds + uses: aws-actions/configure-aws-credentials@v6 + with: + aws-region: us-east-1 + role-duration-seconds: 3600 + role-to-assume: ${{ vars.CDK_ATMOSPHERE_PROD_OIDC_ROLE }} + role-session-name: run-tests@aws-cdk-cli-integ + output-credentials: true + - name: "Run the test suite: ${{ matrix.suite }}" + id: run_the_test_suite_matrix_suite + env: + JSII_SILENCE_WARNING_DEPRECATED_NODE_VERSION: "true" + JSII_SILENCE_WARNING_UNTESTED_NODE_VERSION: "true" + JSII_SILENCE_WARNING_KNOWN_BROKEN_NODE_VERSION: "true" + DOCKERHUB_DISABLED: "true" + CDK_INTEG_ATMOSPHERE_ENABLED: "true" + CDK_INTEG_ATMOSPHERE_ENDPOINT: ${{ vars.CDK_ATMOSPHERE_PROD_ENDPOINT }} + CDK_INTEG_ATMOSPHERE_POOL: ${{ vars.CDK_INTEG_ATMOSPHERE_POOL }} + CDK_MAJOR_VERSION: "2" + RELEASE_TAG: latest + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + INTEG_LOGS: logs + run: npx run-suite --shard="${{ matrix.shard }}/12" --use-cli-release=${{ steps.versions.outputs.cli_version }} --framework-version=${{ steps.versions.outputs.lib_version }} ${{ matrix.suite }} + - name: Set workflow summary + id: set_workflow_summary + if: always() + run: |- + if compgen -G "logs/md/*.md" > /dev/null; then + cat logs/md/*.md >> $GITHUB_STEP_SUMMARY; + fi + - name: Slugify artifact id + id: artifactid + if: always() + env: + INPUT: logs-${{ matrix.suite }}-${{ matrix.node }}-${{ matrix.shard }} + run: |- + slug=$(node -p 'process.env.INPUT.replace(/[^a-z0-9._-]/gi, "-")') + echo "slug=$slug" >> "$GITHUB_OUTPUT" + - name: Upload logs + id: logupload + if: always() + uses: actions/upload-artifact@v7 + with: + name: ${{ steps.artifactid.outputs.slug }} + path: logs/ + overwrite: true + - name: Append artifact URL + id: append_artifact_url + if: always() + run: |- + echo "" >> $GITHUB_STEP_SUMMARY + echo "[Logs](${{ steps.logupload.outputs.artifact-url }})" >> $GITHUB_STEP_SUMMARY + strategy: + fail-fast: false + matrix: + suite: + - cli-integ-tests + node: + - lts/* + shard: + - 1 + - 2 + - 3 + - 4 + - 5 + - 6 + - 7 + - 8 + - 9 + - 10 + - 11 + - 12 + integ_toolkit-lib: + needs: prepare + runs-on: aws-cdk_ubuntu-latest_16-core + permissions: + contents: read + id-token: write + environment: run-tests + env: + NODE_NO_WARNINGS: "1" + MAVEN_ARGS: --no-transfer-progress + IS_CANARY: "true" + CI: "true" + defaults: + run: + shell: bash + if: github.event_name != 'merge_group' && !contains(github.event.pull_request.labels.*.name, 'pr/exempt-integ-test') && github.event_name != 'schedule' + steps: + - name: Download artifact + id: download_artifact + uses: actions/download-artifact@v8 + with: + artifact-ids: ${{needs.prepare.outputs.packagesArtifact}} + path: packages + - name: Download artifact + id: download_artifact_2 + uses: actions/download-artifact@v8 + with: + artifact-ids: ${{needs.prepare.outputs.scriptsArtifact}} + path: .projen + - name: Setup Node.js + id: setup_node_js + uses: actions/setup-node@v6 + with: + node-version: ${{ matrix.node }} + package-manager-cache: false + - name: Set up JDK 18 + id: set_up_jdk_18 + if: matrix.suite == 'init-java' || matrix.suite == 'cli-integ-tests' + uses: actions/setup-java@v5 + with: + java-version: "18" + distribution: corretto + - name: Set git identity + id: set_git_identity + run: |- + git config --global user.name "aws-cdk-cli-integ" + git config --global user.email "noreply@example.com" + - name: Prepare Verdaccio + id: prepare_verdaccio + run: chmod +x .projen/prepare-verdaccio.sh && .projen/prepare-verdaccio.sh + - name: Download and install the test artifact + id: download_and_install_the_test_artifact + run: npm install @aws-cdk-testing/cli-integ + - name: Determine latest package versions + id: versions + run: |- + CLI_VERSION=$(cd ${TMPDIR:-/tmp} && npm view aws-cdk version) + echo "CLI version: ${CLI_VERSION}" + echo "cli_version=${CLI_VERSION}" >> $GITHUB_OUTPUT + LIB_VERSION=$(cd ${TMPDIR:-/tmp} && npm view aws-cdk-lib version) + echo "lib version: ${LIB_VERSION}" + echo "lib_version=${LIB_VERSION}" >> $GITHUB_OUTPUT + - name: Authenticate Via OIDC Role + id: creds + uses: aws-actions/configure-aws-credentials@v6 + with: + aws-region: us-east-1 + role-duration-seconds: 3600 + role-to-assume: ${{ vars.CDK_ATMOSPHERE_PROD_OIDC_ROLE }} + role-session-name: run-tests@aws-cdk-cli-integ + output-credentials: true + - name: "Run the test suite: ${{ matrix.suite }}" + id: run_the_test_suite_matrix_suite + env: + JSII_SILENCE_WARNING_DEPRECATED_NODE_VERSION: "true" + JSII_SILENCE_WARNING_UNTESTED_NODE_VERSION: "true" + JSII_SILENCE_WARNING_KNOWN_BROKEN_NODE_VERSION: "true" + DOCKERHUB_DISABLED: "true" + CDK_INTEG_ATMOSPHERE_ENABLED: "true" + CDK_INTEG_ATMOSPHERE_ENDPOINT: ${{ vars.CDK_ATMOSPHERE_PROD_ENDPOINT }} + CDK_INTEG_ATMOSPHERE_POOL: ${{ vars.CDK_INTEG_ATMOSPHERE_POOL }} + CDK_MAJOR_VERSION: "2" + RELEASE_TAG: latest + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + INTEG_LOGS: logs + run: npx run-suite --use-cli-release=${{ steps.versions.outputs.cli_version }} --framework-version=${{ steps.versions.outputs.lib_version }} ${{ matrix.suite }} + - name: Set workflow summary + id: set_workflow_summary + if: always() + run: |- + if compgen -G "logs/md/*.md" > /dev/null; then + cat logs/md/*.md >> $GITHUB_STEP_SUMMARY; + fi + - name: Slugify artifact id + id: artifactid + if: always() + env: + INPUT: logs-${{ matrix.suite }}-${{ matrix.node }} + run: |- + slug=$(node -p 'process.env.INPUT.replace(/[^a-z0-9._-]/gi, "-")') + echo "slug=$slug" >> "$GITHUB_OUTPUT" + - name: Upload logs + id: logupload + if: always() + uses: actions/upload-artifact@v7 + with: + name: ${{ steps.artifactid.outputs.slug }} + path: logs/ + overwrite: true + - name: Append artifact URL + id: append_artifact_url + if: always() + run: |- + echo "" >> $GITHUB_STEP_SUMMARY + echo "[Logs](${{ steps.logupload.outputs.artifact-url }})" >> $GITHUB_STEP_SUMMARY + strategy: + fail-fast: false + matrix: + suite: + - toolkit-lib-integ-tests + node: + - lts/* + - "20" + - "22" + - "24" + integ_telemetry: + needs: prepare + runs-on: aws-cdk_ubuntu-latest_16-core + permissions: + contents: read + id-token: write + environment: run-tests + env: + NODE_NO_WARNINGS: "1" + MAVEN_ARGS: --no-transfer-progress + IS_CANARY: "true" + CI: "true" + defaults: + run: + shell: bash + if: github.event_name != 'merge_group' && !contains(github.event.pull_request.labels.*.name, 'pr/exempt-integ-test') && github.event_name != 'schedule' + steps: + - name: Download artifact + id: download_artifact + uses: actions/download-artifact@v8 + with: + artifact-ids: ${{needs.prepare.outputs.packagesArtifact}} + path: packages + - name: Download artifact + id: download_artifact_2 + uses: actions/download-artifact@v8 + with: + artifact-ids: ${{needs.prepare.outputs.scriptsArtifact}} + path: .projen + - name: Setup Node.js + id: setup_node_js + uses: actions/setup-node@v6 + with: + node-version: ${{ matrix.node }} + package-manager-cache: false + - name: Set up JDK 18 + id: set_up_jdk_18 + if: matrix.suite == 'init-java' || matrix.suite == 'cli-integ-tests' + uses: actions/setup-java@v5 + with: + java-version: "18" + distribution: corretto + - name: Set git identity + id: set_git_identity + run: |- + git config --global user.name "aws-cdk-cli-integ" + git config --global user.email "noreply@example.com" + - name: Prepare Verdaccio + id: prepare_verdaccio + run: chmod +x .projen/prepare-verdaccio.sh && .projen/prepare-verdaccio.sh + - name: Download and install the test artifact + id: download_and_install_the_test_artifact + run: npm install @aws-cdk-testing/cli-integ + - name: Determine latest package versions + id: versions + run: |- + CLI_VERSION=$(cd ${TMPDIR:-/tmp} && npm view aws-cdk version) + echo "CLI version: ${CLI_VERSION}" + echo "cli_version=${CLI_VERSION}" >> $GITHUB_OUTPUT + LIB_VERSION=$(cd ${TMPDIR:-/tmp} && npm view aws-cdk-lib version) + echo "lib version: ${LIB_VERSION}" + echo "lib_version=${LIB_VERSION}" >> $GITHUB_OUTPUT + - name: Authenticate Via OIDC Role + id: creds + uses: aws-actions/configure-aws-credentials@v6 + with: + aws-region: us-east-1 + role-duration-seconds: 3600 + role-to-assume: ${{ vars.CDK_ATMOSPHERE_PROD_OIDC_ROLE }} + role-session-name: run-tests@aws-cdk-cli-integ + output-credentials: true + - name: "Run the test suite: ${{ matrix.suite }}" + id: run_the_test_suite_matrix_suite + env: + JSII_SILENCE_WARNING_DEPRECATED_NODE_VERSION: "true" + JSII_SILENCE_WARNING_UNTESTED_NODE_VERSION: "true" + JSII_SILENCE_WARNING_KNOWN_BROKEN_NODE_VERSION: "true" + DOCKERHUB_DISABLED: "true" + CDK_INTEG_ATMOSPHERE_ENABLED: "true" + CDK_INTEG_ATMOSPHERE_ENDPOINT: ${{ vars.CDK_ATMOSPHERE_PROD_ENDPOINT }} + CDK_INTEG_ATMOSPHERE_POOL: ${{ vars.CDK_INTEG_ATMOSPHERE_POOL }} + CDK_MAJOR_VERSION: "2" + RELEASE_TAG: latest + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + INTEG_LOGS: logs + run: npx run-suite --use-cli-release=${{ steps.versions.outputs.cli_version }} --framework-version=${{ steps.versions.outputs.lib_version }} ${{ matrix.suite }} + - name: Set workflow summary + id: set_workflow_summary + if: always() + run: |- + if compgen -G "logs/md/*.md" > /dev/null; then + cat logs/md/*.md >> $GITHUB_STEP_SUMMARY; + fi + - name: Slugify artifact id + id: artifactid + if: always() + env: + INPUT: logs-${{ matrix.suite }}-${{ matrix.node }} + run: |- + slug=$(node -p 'process.env.INPUT.replace(/[^a-z0-9._-]/gi, "-")') + echo "slug=$slug" >> "$GITHUB_OUTPUT" + - name: Upload logs + id: logupload + if: always() + uses: actions/upload-artifact@v7 + with: + name: ${{ steps.artifactid.outputs.slug }} + path: logs/ + overwrite: true + - name: Append artifact URL + id: append_artifact_url + if: always() + run: |- + echo "" >> $GITHUB_STEP_SUMMARY + echo "[Logs](${{ steps.logupload.outputs.artifact-url }})" >> $GITHUB_STEP_SUMMARY + strategy: + fail-fast: false + matrix: + suite: + - telemetry-integ-tests + node: + - lts/* + integ_init-templates: + needs: prepare + runs-on: aws-cdk_ubuntu-latest_16-core + permissions: + contents: read + id-token: write + environment: run-tests + env: + NODE_NO_WARNINGS: "1" + MAVEN_ARGS: --no-transfer-progress + IS_CANARY: "true" + CI: "true" + defaults: + run: + shell: bash + if: github.event_name != 'merge_group' && !contains(github.event.pull_request.labels.*.name, 'pr/exempt-integ-test') && github.event_name != 'schedule' + steps: + - name: Download artifact + id: download_artifact + uses: actions/download-artifact@v8 + with: + artifact-ids: ${{needs.prepare.outputs.packagesArtifact}} + path: packages + - name: Download artifact + id: download_artifact_2 + uses: actions/download-artifact@v8 + with: + artifact-ids: ${{needs.prepare.outputs.scriptsArtifact}} + path: .projen + - name: Setup Node.js + id: setup_node_js + uses: actions/setup-node@v6 + with: + node-version: ${{ matrix.node }} + package-manager-cache: false + - name: Set up JDK 18 + id: set_up_jdk_18 + if: matrix.suite == 'init-java' || matrix.suite == 'cli-integ-tests' + uses: actions/setup-java@v5 + with: + java-version: "18" + distribution: corretto + - name: Set git identity + id: set_git_identity + run: |- + git config --global user.name "aws-cdk-cli-integ" + git config --global user.email "noreply@example.com" + - name: Prepare Verdaccio + id: prepare_verdaccio + run: chmod +x .projen/prepare-verdaccio.sh && .projen/prepare-verdaccio.sh + - name: Download and install the test artifact + id: download_and_install_the_test_artifact + run: npm install @aws-cdk-testing/cli-integ + - name: Determine latest package versions + id: versions + run: |- + CLI_VERSION=$(cd ${TMPDIR:-/tmp} && npm view aws-cdk version) + echo "CLI version: ${CLI_VERSION}" + echo "cli_version=${CLI_VERSION}" >> $GITHUB_OUTPUT + LIB_VERSION=$(cd ${TMPDIR:-/tmp} && npm view aws-cdk-lib version) + echo "lib version: ${LIB_VERSION}" + echo "lib_version=${LIB_VERSION}" >> $GITHUB_OUTPUT + - name: Authenticate Via OIDC Role + id: creds + uses: aws-actions/configure-aws-credentials@v6 + with: + aws-region: us-east-1 + role-duration-seconds: 3600 + role-to-assume: ${{ vars.CDK_ATMOSPHERE_PROD_OIDC_ROLE }} + role-session-name: run-tests@aws-cdk-cli-integ + output-credentials: true + - name: "Run the test suite: ${{ matrix.suite }}" + id: run_the_test_suite_matrix_suite + env: + JSII_SILENCE_WARNING_DEPRECATED_NODE_VERSION: "true" + JSII_SILENCE_WARNING_UNTESTED_NODE_VERSION: "true" + JSII_SILENCE_WARNING_KNOWN_BROKEN_NODE_VERSION: "true" + DOCKERHUB_DISABLED: "true" + CDK_INTEG_ATMOSPHERE_ENABLED: "true" + CDK_INTEG_ATMOSPHERE_ENDPOINT: ${{ vars.CDK_ATMOSPHERE_PROD_ENDPOINT }} + CDK_INTEG_ATMOSPHERE_POOL: ${{ vars.CDK_INTEG_ATMOSPHERE_POOL }} + CDK_MAJOR_VERSION: "2" + RELEASE_TAG: latest + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + INTEG_LOGS: logs + run: npx run-suite --use-cli-release=${{ steps.versions.outputs.cli_version }} --framework-version=${{ steps.versions.outputs.lib_version }} ${{ matrix.suite }} + - name: Set workflow summary + id: set_workflow_summary + if: always() + run: |- + if compgen -G "logs/md/*.md" > /dev/null; then + cat logs/md/*.md >> $GITHUB_STEP_SUMMARY; + fi + - name: Slugify artifact id + id: artifactid + if: always() + env: + INPUT: logs-${{ matrix.suite }}-${{ matrix.node }} + run: |- + slug=$(node -p 'process.env.INPUT.replace(/[^a-z0-9._-]/gi, "-")') + echo "slug=$slug" >> "$GITHUB_OUTPUT" + - name: Upload logs + id: logupload + if: always() + uses: actions/upload-artifact@v7 + with: + name: ${{ steps.artifactid.outputs.slug }} + path: logs/ + overwrite: true + - name: Append artifact URL + id: append_artifact_url + if: always() + run: |- + echo "" >> $GITHUB_STEP_SUMMARY + echo "[Logs](${{ steps.logupload.outputs.artifact-url }})" >> $GITHUB_STEP_SUMMARY + strategy: + fail-fast: false + matrix: + include: + - suite: init-typescript-app + node: "20" + - suite: init-typescript-app + node: "22" + - suite: init-typescript-app + node: "24" + suite: + - init-csharp + - init-fsharp + - init-go + - init-java + - init-javascript + - init-python + - init-typescript-app + - init-typescript-lib + node: + - lts/* + integ_tool-integrations: + needs: prepare + runs-on: aws-cdk_ubuntu-latest_16-core + permissions: + contents: read + id-token: write + environment: run-tests + env: + NODE_NO_WARNINGS: "1" + MAVEN_ARGS: --no-transfer-progress + IS_CANARY: "true" + CI: "true" + defaults: + run: + shell: bash + if: github.event_name != 'merge_group' && !contains(github.event.pull_request.labels.*.name, 'pr/exempt-integ-test') && github.event_name != 'schedule' steps: - name: Download artifact id: download_artifact @@ -178,7 +708,7 @@ jobs: RELEASE_TAG: latest GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} INTEG_LOGS: logs - run: npx run-suite --shard="${{ matrix.shard }}/12" --use-cli-release=${{ steps.versions.outputs.cli_version }} --framework-version=${{ steps.versions.outputs.lib_version }} ${{ matrix.suite }} + run: npx run-suite --use-cli-release=${{ steps.versions.outputs.cli_version }} --framework-version=${{ steps.versions.outputs.lib_version }} ${{ matrix.suite }} - name: Set workflow summary id: set_workflow_summary if: always() @@ -190,7 +720,166 @@ jobs: id: artifactid if: always() env: - INPUT: logs-${{ matrix.suite }}-${{ matrix.node }}-${{ matrix.shard }} + INPUT: logs-${{ matrix.suite }}-${{ matrix.node }} + run: |- + slug=$(node -p 'process.env.INPUT.replace(/[^a-z0-9._-]/gi, "-")') + echo "slug=$slug" >> "$GITHUB_OUTPUT" + - name: Upload logs + id: logupload + if: always() + uses: actions/upload-artifact@v7 + with: + name: ${{ steps.artifactid.outputs.slug }} + path: logs/ + overwrite: true + - name: Append artifact URL + id: append_artifact_url + if: always() + run: |- + echo "" >> $GITHUB_STEP_SUMMARY + echo "[Logs](${{ steps.logupload.outputs.artifact-url }})" >> $GITHUB_STEP_SUMMARY + strategy: + fail-fast: false + matrix: + suite: + - tool-integrations + node: + - "20" + integ_cli_windows: + needs: prepare + runs-on: windows-latest + permissions: + contents: read + id-token: write + environment: run-tests + env: + NODE_NO_WARNINGS: "1" + MAVEN_ARGS: --no-transfer-progress + IS_CANARY: "true" + CI: "true" + CDK_INTEG_SKIP_TESTS_FILE: ${{ github.workspace }}\windows-skip-tests.txt + defaults: + run: + shell: bash + if: github.event_name != 'merge_group' && !contains(github.event.pull_request.labels.*.name, 'pr/exempt-integ-test') && (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' || contains(github.event.pull_request.labels.*.name, 'pr/test-windows')) + steps: + - name: Set up Dev Drive for TEMP and npm cache + id: set_up_dev_drive_for_temp_and_npm_cache + run: |- + $vhd = "C:\devdrive.vhdx" + $drive = (New-VHD -Path $vhd -SizeBytes 40GB -Dynamic | Mount-VHD -PassThru | Initialize-Disk -PassThru | New-Partition -AssignDriveLetter -UseMaximumSize | Format-Volume -DevDrive -Confirm:$false).DriveLetter + New-Item -ItemType Directory -Path "${drive}:\temp" | Out-Null + New-Item -ItemType Directory -Path "${drive}:\npm-cache" | Out-Null + echo "TEMP=${drive}:\temp" >> $env:GITHUB_ENV + echo "TMP=${drive}:\temp" >> $env:GITHUB_ENV + echo "npm_config_cache=${drive}:\npm-cache" >> $env:GITHUB_ENV + shell: powershell + - name: Write Windows skip-tests file + id: write_windows_skip-tests_file + run: |- + cat > windows-skip-tests.txt << 'EOF' + deploy same docker asset to multiple regions + deploy same docker asset to multiple stacks + deploy stack with multiple docker assets + deploy stack with docker asset + cdk-assets smoke test + deploy new style synthesis to new style bootstrap (with docker image) + Garbage Collection untags in-use ecr images + Garbage Collection keeps in use ecr images + Garbage Collection deletes unused ecr images + Garbage Collection tags unused ecr images + all calls from isolated container go through proxy + docker-credential-cdk-assets can assume role and fetch ECR credentials + toolkit deploy stack with multiple docker assets + CDK synth bundled functions as expected + CDK synth add the metadata properties expected by sam + can deploy with session tags on the deploy, lookup, file asset, and image asset publishing roles + generating and loading assembly + test resource import with construct that requires bundling + hotswap deployment supports Bedrock AgentCore Runtime + sam can locally test the synthesized cdk application + EOF + - name: Download artifact + id: download_artifact + uses: actions/download-artifact@v8 + with: + artifact-ids: ${{needs.prepare.outputs.packagesArtifact}} + path: packages + - name: Download artifact + id: download_artifact_2 + uses: actions/download-artifact@v8 + with: + artifact-ids: ${{needs.prepare.outputs.scriptsArtifact}} + path: .projen + - name: Setup Node.js + id: setup_node_js + uses: actions/setup-node@v6 + with: + node-version: ${{ matrix.node }} + package-manager-cache: false + - name: Set up JDK 18 + id: set_up_jdk_18 + if: matrix.suite == 'init-java' || matrix.suite == 'cli-integ-tests' + uses: actions/setup-java@v5 + with: + java-version: "18" + distribution: corretto + - name: Set git identity + id: set_git_identity + run: |- + git config --global user.name "aws-cdk-cli-integ" + git config --global user.email "noreply@example.com" + - name: Prepare Verdaccio + id: prepare_verdaccio + run: chmod +x .projen/prepare-verdaccio.sh && .projen/prepare-verdaccio.sh + - name: Download and install the test artifact + id: download_and_install_the_test_artifact + run: npm install @aws-cdk-testing/cli-integ + - name: Determine latest package versions + id: versions + run: |- + CLI_VERSION=$(cd ${TMPDIR:-/tmp} && npm view aws-cdk version) + echo "CLI version: ${CLI_VERSION}" + echo "cli_version=${CLI_VERSION}" >> $GITHUB_OUTPUT + LIB_VERSION=$(cd ${TMPDIR:-/tmp} && npm view aws-cdk-lib version) + echo "lib version: ${LIB_VERSION}" + echo "lib_version=${LIB_VERSION}" >> $GITHUB_OUTPUT + - name: Authenticate Via OIDC Role + id: creds + uses: aws-actions/configure-aws-credentials@v6 + with: + aws-region: us-east-1 + role-duration-seconds: 3600 + role-to-assume: ${{ vars.CDK_ATMOSPHERE_PROD_OIDC_ROLE }} + role-session-name: run-tests@aws-cdk-cli-integ + output-credentials: true + - name: "Run the test suite: ${{ matrix.suite }}" + id: run_the_test_suite_matrix_suite + env: + JSII_SILENCE_WARNING_DEPRECATED_NODE_VERSION: "true" + JSII_SILENCE_WARNING_UNTESTED_NODE_VERSION: "true" + JSII_SILENCE_WARNING_KNOWN_BROKEN_NODE_VERSION: "true" + DOCKERHUB_DISABLED: "true" + CDK_INTEG_ATMOSPHERE_ENABLED: "true" + CDK_INTEG_ATMOSPHERE_ENDPOINT: ${{ vars.CDK_ATMOSPHERE_PROD_ENDPOINT }} + CDK_INTEG_ATMOSPHERE_POOL: ${{ vars.CDK_INTEG_ATMOSPHERE_POOL }} + CDK_MAJOR_VERSION: "2" + RELEASE_TAG: latest + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + INTEG_LOGS: logs + run: npx run-suite --shard="${{ matrix.shard }}/24" --use-cli-release=${{ steps.versions.outputs.cli_version }} --framework-version=${{ steps.versions.outputs.lib_version }} ${{ matrix.suite }} + - name: Set workflow summary + id: set_workflow_summary + if: always() + run: |- + if compgen -G "logs/md/*.md" > /dev/null; then + cat logs/md/*.md >> $GITHUB_STEP_SUMMARY; + fi + - name: Slugify artifact id + id: artifactid + if: always() + env: + INPUT: logs_windows-${{ matrix.suite }}-${{ matrix.node }}-${{ matrix.shard }} run: |- slug=$(node -p 'process.env.INPUT.replace(/[^a-z0-9._-]/gi, "-")') echo "slug=$slug" >> "$GITHUB_OUTPUT" @@ -208,6 +897,7 @@ jobs: run: |- echo "" >> $GITHUB_STEP_SUMMARY echo "[Logs](${{ steps.logupload.outputs.artifact-url }})" >> $GITHUB_STEP_SUMMARY + timeout-minutes: 90 strategy: fail-fast: false matrix: @@ -228,9 +918,21 @@ jobs: - 10 - 11 - 12 - integ_toolkit-lib: + - 13 + - 14 + - 15 + - 16 + - 17 + - 18 + - 19 + - 20 + - 21 + - 22 + - 23 + - 24 + integ_toolkit-lib_windows: needs: prepare - runs-on: aws-cdk_ubuntu-latest_16-core + runs-on: windows-latest permissions: contents: read id-token: write @@ -240,8 +942,48 @@ jobs: MAVEN_ARGS: --no-transfer-progress IS_CANARY: "true" CI: "true" - if: github.event_name != 'merge_group' && !contains(github.event.pull_request.labels.*.name, 'pr/exempt-integ-test') + CDK_INTEG_SKIP_TESTS_FILE: ${{ github.workspace }}\windows-skip-tests.txt + defaults: + run: + shell: bash + if: github.event_name != 'merge_group' && !contains(github.event.pull_request.labels.*.name, 'pr/exempt-integ-test') && (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' || contains(github.event.pull_request.labels.*.name, 'pr/test-windows')) steps: + - name: Set up Dev Drive for TEMP and npm cache + id: set_up_dev_drive_for_temp_and_npm_cache + run: |- + $vhd = "C:\devdrive.vhdx" + $drive = (New-VHD -Path $vhd -SizeBytes 40GB -Dynamic | Mount-VHD -PassThru | Initialize-Disk -PassThru | New-Partition -AssignDriveLetter -UseMaximumSize | Format-Volume -DevDrive -Confirm:$false).DriveLetter + New-Item -ItemType Directory -Path "${drive}:\temp" | Out-Null + New-Item -ItemType Directory -Path "${drive}:\npm-cache" | Out-Null + echo "TEMP=${drive}:\temp" >> $env:GITHUB_ENV + echo "TMP=${drive}:\temp" >> $env:GITHUB_ENV + echo "npm_config_cache=${drive}:\npm-cache" >> $env:GITHUB_ENV + shell: powershell + - name: Write Windows skip-tests file + id: write_windows_skip-tests_file + run: |- + cat > windows-skip-tests.txt << 'EOF' + deploy same docker asset to multiple regions + deploy same docker asset to multiple stacks + deploy stack with multiple docker assets + deploy stack with docker asset + cdk-assets smoke test + deploy new style synthesis to new style bootstrap (with docker image) + Garbage Collection untags in-use ecr images + Garbage Collection keeps in use ecr images + Garbage Collection deletes unused ecr images + Garbage Collection tags unused ecr images + all calls from isolated container go through proxy + docker-credential-cdk-assets can assume role and fetch ECR credentials + toolkit deploy stack with multiple docker assets + CDK synth bundled functions as expected + CDK synth add the metadata properties expected by sam + can deploy with session tags on the deploy, lookup, file asset, and image asset publishing roles + generating and loading assembly + test resource import with construct that requires bundling + hotswap deployment supports Bedrock AgentCore Runtime + sam can locally test the synthesized cdk application + EOF - name: Download artifact id: download_artifact uses: actions/download-artifact@v8 @@ -322,7 +1064,7 @@ jobs: id: artifactid if: always() env: - INPUT: logs-${{ matrix.suite }}-${{ matrix.node }} + INPUT: logs_windows-${{ matrix.suite }}-${{ matrix.node }} run: |- slug=$(node -p 'process.env.INPUT.replace(/[^a-z0-9._-]/gi, "-")') echo "slug=$slug" >> "$GITHUB_OUTPUT" @@ -340,6 +1082,7 @@ jobs: run: |- echo "" >> $GITHUB_STEP_SUMMARY echo "[Logs](${{ steps.logupload.outputs.artifact-url }})" >> $GITHUB_STEP_SUMMARY + timeout-minutes: 90 strategy: fail-fast: false matrix: @@ -350,9 +1093,9 @@ jobs: - "20" - "22" - "24" - integ_telemetry: + integ_telemetry_windows: needs: prepare - runs-on: aws-cdk_ubuntu-latest_16-core + runs-on: windows-latest permissions: contents: read id-token: write @@ -362,8 +1105,48 @@ jobs: MAVEN_ARGS: --no-transfer-progress IS_CANARY: "true" CI: "true" - if: github.event_name != 'merge_group' && !contains(github.event.pull_request.labels.*.name, 'pr/exempt-integ-test') + CDK_INTEG_SKIP_TESTS_FILE: ${{ github.workspace }}\windows-skip-tests.txt + defaults: + run: + shell: bash + if: github.event_name != 'merge_group' && !contains(github.event.pull_request.labels.*.name, 'pr/exempt-integ-test') && (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' || contains(github.event.pull_request.labels.*.name, 'pr/test-windows')) steps: + - name: Set up Dev Drive for TEMP and npm cache + id: set_up_dev_drive_for_temp_and_npm_cache + run: |- + $vhd = "C:\devdrive.vhdx" + $drive = (New-VHD -Path $vhd -SizeBytes 40GB -Dynamic | Mount-VHD -PassThru | Initialize-Disk -PassThru | New-Partition -AssignDriveLetter -UseMaximumSize | Format-Volume -DevDrive -Confirm:$false).DriveLetter + New-Item -ItemType Directory -Path "${drive}:\temp" | Out-Null + New-Item -ItemType Directory -Path "${drive}:\npm-cache" | Out-Null + echo "TEMP=${drive}:\temp" >> $env:GITHUB_ENV + echo "TMP=${drive}:\temp" >> $env:GITHUB_ENV + echo "npm_config_cache=${drive}:\npm-cache" >> $env:GITHUB_ENV + shell: powershell + - name: Write Windows skip-tests file + id: write_windows_skip-tests_file + run: |- + cat > windows-skip-tests.txt << 'EOF' + deploy same docker asset to multiple regions + deploy same docker asset to multiple stacks + deploy stack with multiple docker assets + deploy stack with docker asset + cdk-assets smoke test + deploy new style synthesis to new style bootstrap (with docker image) + Garbage Collection untags in-use ecr images + Garbage Collection keeps in use ecr images + Garbage Collection deletes unused ecr images + Garbage Collection tags unused ecr images + all calls from isolated container go through proxy + docker-credential-cdk-assets can assume role and fetch ECR credentials + toolkit deploy stack with multiple docker assets + CDK synth bundled functions as expected + CDK synth add the metadata properties expected by sam + can deploy with session tags on the deploy, lookup, file asset, and image asset publishing roles + generating and loading assembly + test resource import with construct that requires bundling + hotswap deployment supports Bedrock AgentCore Runtime + sam can locally test the synthesized cdk application + EOF - name: Download artifact id: download_artifact uses: actions/download-artifact@v8 @@ -444,7 +1227,7 @@ jobs: id: artifactid if: always() env: - INPUT: logs-${{ matrix.suite }}-${{ matrix.node }} + INPUT: logs_windows-${{ matrix.suite }}-${{ matrix.node }} run: |- slug=$(node -p 'process.env.INPUT.replace(/[^a-z0-9._-]/gi, "-")') echo "slug=$slug" >> "$GITHUB_OUTPUT" @@ -462,6 +1245,7 @@ jobs: run: |- echo "" >> $GITHUB_STEP_SUMMARY echo "[Logs](${{ steps.logupload.outputs.artifact-url }})" >> $GITHUB_STEP_SUMMARY + timeout-minutes: 90 strategy: fail-fast: false matrix: @@ -469,9 +1253,9 @@ jobs: - telemetry-integ-tests node: - lts/* - integ_init-templates: + integ_init-templates_windows: needs: prepare - runs-on: aws-cdk_ubuntu-latest_16-core + runs-on: windows-latest permissions: contents: read id-token: write @@ -481,8 +1265,48 @@ jobs: MAVEN_ARGS: --no-transfer-progress IS_CANARY: "true" CI: "true" - if: github.event_name != 'merge_group' && !contains(github.event.pull_request.labels.*.name, 'pr/exempt-integ-test') + CDK_INTEG_SKIP_TESTS_FILE: ${{ github.workspace }}\windows-skip-tests.txt + defaults: + run: + shell: bash + if: github.event_name != 'merge_group' && !contains(github.event.pull_request.labels.*.name, 'pr/exempt-integ-test') && (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' || contains(github.event.pull_request.labels.*.name, 'pr/test-windows')) steps: + - name: Set up Dev Drive for TEMP and npm cache + id: set_up_dev_drive_for_temp_and_npm_cache + run: |- + $vhd = "C:\devdrive.vhdx" + $drive = (New-VHD -Path $vhd -SizeBytes 40GB -Dynamic | Mount-VHD -PassThru | Initialize-Disk -PassThru | New-Partition -AssignDriveLetter -UseMaximumSize | Format-Volume -DevDrive -Confirm:$false).DriveLetter + New-Item -ItemType Directory -Path "${drive}:\temp" | Out-Null + New-Item -ItemType Directory -Path "${drive}:\npm-cache" | Out-Null + echo "TEMP=${drive}:\temp" >> $env:GITHUB_ENV + echo "TMP=${drive}:\temp" >> $env:GITHUB_ENV + echo "npm_config_cache=${drive}:\npm-cache" >> $env:GITHUB_ENV + shell: powershell + - name: Write Windows skip-tests file + id: write_windows_skip-tests_file + run: |- + cat > windows-skip-tests.txt << 'EOF' + deploy same docker asset to multiple regions + deploy same docker asset to multiple stacks + deploy stack with multiple docker assets + deploy stack with docker asset + cdk-assets smoke test + deploy new style synthesis to new style bootstrap (with docker image) + Garbage Collection untags in-use ecr images + Garbage Collection keeps in use ecr images + Garbage Collection deletes unused ecr images + Garbage Collection tags unused ecr images + all calls from isolated container go through proxy + docker-credential-cdk-assets can assume role and fetch ECR credentials + toolkit deploy stack with multiple docker assets + CDK synth bundled functions as expected + CDK synth add the metadata properties expected by sam + can deploy with session tags on the deploy, lookup, file asset, and image asset publishing roles + generating and loading assembly + test resource import with construct that requires bundling + hotswap deployment supports Bedrock AgentCore Runtime + sam can locally test the synthesized cdk application + EOF - name: Download artifact id: download_artifact uses: actions/download-artifact@v8 @@ -563,7 +1387,7 @@ jobs: id: artifactid if: always() env: - INPUT: logs-${{ matrix.suite }}-${{ matrix.node }} + INPUT: logs_windows-${{ matrix.suite }}-${{ matrix.node }} run: |- slug=$(node -p 'process.env.INPUT.replace(/[^a-z0-9._-]/gi, "-")') echo "slug=$slug" >> "$GITHUB_OUTPUT" @@ -581,6 +1405,7 @@ jobs: run: |- echo "" >> $GITHUB_STEP_SUMMARY echo "[Logs](${{ steps.logupload.outputs.artifact-url }})" >> $GITHUB_STEP_SUMMARY + timeout-minutes: 90 strategy: fail-fast: false matrix: @@ -602,9 +1427,9 @@ jobs: - init-typescript-lib node: - lts/* - integ_tool-integrations: + integ_tool-integrations_windows: needs: prepare - runs-on: aws-cdk_ubuntu-latest_16-core + runs-on: windows-latest permissions: contents: read id-token: write @@ -614,8 +1439,48 @@ jobs: MAVEN_ARGS: --no-transfer-progress IS_CANARY: "true" CI: "true" - if: github.event_name != 'merge_group' && !contains(github.event.pull_request.labels.*.name, 'pr/exempt-integ-test') + CDK_INTEG_SKIP_TESTS_FILE: ${{ github.workspace }}\windows-skip-tests.txt + defaults: + run: + shell: bash + if: github.event_name != 'merge_group' && !contains(github.event.pull_request.labels.*.name, 'pr/exempt-integ-test') && (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' || contains(github.event.pull_request.labels.*.name, 'pr/test-windows')) steps: + - name: Set up Dev Drive for TEMP and npm cache + id: set_up_dev_drive_for_temp_and_npm_cache + run: |- + $vhd = "C:\devdrive.vhdx" + $drive = (New-VHD -Path $vhd -SizeBytes 40GB -Dynamic | Mount-VHD -PassThru | Initialize-Disk -PassThru | New-Partition -AssignDriveLetter -UseMaximumSize | Format-Volume -DevDrive -Confirm:$false).DriveLetter + New-Item -ItemType Directory -Path "${drive}:\temp" | Out-Null + New-Item -ItemType Directory -Path "${drive}:\npm-cache" | Out-Null + echo "TEMP=${drive}:\temp" >> $env:GITHUB_ENV + echo "TMP=${drive}:\temp" >> $env:GITHUB_ENV + echo "npm_config_cache=${drive}:\npm-cache" >> $env:GITHUB_ENV + shell: powershell + - name: Write Windows skip-tests file + id: write_windows_skip-tests_file + run: |- + cat > windows-skip-tests.txt << 'EOF' + deploy same docker asset to multiple regions + deploy same docker asset to multiple stacks + deploy stack with multiple docker assets + deploy stack with docker asset + cdk-assets smoke test + deploy new style synthesis to new style bootstrap (with docker image) + Garbage Collection untags in-use ecr images + Garbage Collection keeps in use ecr images + Garbage Collection deletes unused ecr images + Garbage Collection tags unused ecr images + all calls from isolated container go through proxy + docker-credential-cdk-assets can assume role and fetch ECR credentials + toolkit deploy stack with multiple docker assets + CDK synth bundled functions as expected + CDK synth add the metadata properties expected by sam + can deploy with session tags on the deploy, lookup, file asset, and image asset publishing roles + generating and loading assembly + test resource import with construct that requires bundling + hotswap deployment supports Bedrock AgentCore Runtime + sam can locally test the synthesized cdk application + EOF - name: Download artifact id: download_artifact uses: actions/download-artifact@v8 @@ -696,7 +1561,7 @@ jobs: id: artifactid if: always() env: - INPUT: logs-${{ matrix.suite }}-${{ matrix.node }} + INPUT: logs_windows-${{ matrix.suite }}-${{ matrix.node }} run: |- slug=$(node -p 'process.env.INPUT.replace(/[^a-z0-9._-]/gi, "-")') echo "slug=$slug" >> "$GITHUB_OUTPUT" @@ -714,6 +1579,7 @@ jobs: run: |- echo "" >> $GITHUB_STEP_SUMMARY echo "[Logs](${{ steps.logupload.outputs.artifact-url }})" >> $GITHUB_STEP_SUMMARY + timeout-minutes: 90 strategy: fail-fast: false matrix: @@ -728,6 +1594,11 @@ jobs: - integ_telemetry - integ_init-templates - integ_tool-integrations + - integ_cli_windows + - integ_toolkit-lib_windows + - integ_telemetry_windows + - integ_init-templates_windows + - integ_tool-integrations_windows runs-on: ubuntu-latest permissions: {} if: always() @@ -747,7 +1618,65 @@ jobs: - name: integ_tool-integrations result id: integ_tool-integrations_result run: echo ${{ needs.integ_tool-integrations.result }} + - name: integ_cli_windows result + id: integ_cli_windows_result + run: echo ${{ needs.integ_cli_windows.result }} + - name: integ_toolkit-lib_windows result + id: integ_toolkit-lib_windows_result + run: echo ${{ needs.integ_toolkit-lib_windows.result }} + - name: integ_telemetry_windows result + id: integ_telemetry_windows_result + run: echo ${{ needs.integ_telemetry_windows.result }} + - name: integ_init-templates_windows result + id: integ_init-templates_windows_result + run: echo ${{ needs.integ_init-templates_windows.result }} + - name: integ_tool-integrations_windows result + id: integ_tool-integrations_windows_result + run: echo ${{ needs.integ_tool-integrations_windows.result }} - name: Set status based on test results id: set_status_based_on_test_results - if: ${{ !(contains(fromJSON('["success", "skipped"]'), needs.integ_cli.result) && contains(fromJSON('["success", "skipped"]'), needs.integ_toolkit-lib.result) && contains(fromJSON('["success", "skipped"]'), needs.integ_telemetry.result) && contains(fromJSON('["success", "skipped"]'), needs.integ_init-templates.result) && contains(fromJSON('["success", "skipped"]'), needs.integ_tool-integrations.result)) }} + if: ${{ !(contains(fromJSON('["success", "skipped"]'), needs.integ_cli.result) && contains(fromJSON('["success", "skipped"]'), needs.integ_toolkit-lib.result) && contains(fromJSON('["success", "skipped"]'), needs.integ_telemetry.result) && contains(fromJSON('["success", "skipped"]'), needs.integ_init-templates.result) && contains(fromJSON('["success", "skipped"]'), needs.integ_tool-integrations.result) && contains(fromJSON('["success", "skipped"]'), needs.integ_cli_windows.result) && contains(fromJSON('["success", "skipped"]'), needs.integ_toolkit-lib_windows.result) && contains(fromJSON('["success", "skipped"]'), needs.integ_telemetry_windows.result) && contains(fromJSON('["success", "skipped"]'), needs.integ_init-templates_windows.result) && contains(fromJSON('["success", "skipped"]'), needs.integ_tool-integrations_windows.result)) }} run: exit 1 + integ_windows_report_failure: + needs: + - integ_cli_windows + - integ_toolkit-lib_windows + - integ_telemetry_windows + - integ_init-templates_windows + - integ_tool-integrations_windows + runs-on: ubuntu-latest + permissions: + contents: read + issues: write + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_REPO: ${{ github.repository }} + RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + if: ${{ always() && github.event_name == 'schedule' && contains(needs.*.result, 'failure') }} + steps: + - name: File or update the tracking issue + id: file_or_update_the_tracking_issue + run: |- + set -euo pipefail + + BODY="Nightly Windows integ run failed: $RUN_URL" + + # --jq '.[0].number // empty' yields an empty string when no issue is open, + # which distinguishes "nothing found" from a real issue number. + EXISTING=$(gh issue list \ + --label 'windows-integ-nightly' \ + --state open \ + --limit 1 \ + --json number \ + --jq '.[0].number // empty') + + if [ -n "$EXISTING" ]; then + echo "Commenting on existing issue #$EXISTING" + gh issue comment "$EXISTING" --body "$BODY" + else + echo "Filing a new issue" + gh issue create \ + --title 'Windows integ nightly is failing' \ + --label 'windows-integ-nightly' \ + --body "$BODY" + fi diff --git a/projenrc/cdk-cli-integ-tests.ts b/projenrc/cdk-cli-integ-tests.ts index 59791914a..f0c1aeed1 100644 --- a/projenrc/cdk-cli-integ-tests.ts +++ b/projenrc/cdk-cli-integ-tests.ts @@ -12,6 +12,45 @@ export function fixupTestTask(project: Project, taskName = 'test'): void { const NOT_FLAGGED_EXPR = "!contains(github.event.pull_request.labels.*.name, 'pr/exempt-integ-test')"; +/** + * Label that opts a pull request into the Windows integ suites. + * + * Windows coverage is expensive and slow, so it does not run on every PR. Add + * this label to a PR that touches platform-sensitive code (paths, subprocess + * spawning, shell quoting) to run it; a failure then blocks the PR like any + * other integ failure. + * + * MUST exist in the repository's label set, otherwise `gh issue create` in the + * nightly failure report will fail. + */ +const WINDOWS_LABEL = 'pr/test-windows'; + +/** + * Marker label on the issue that tracks nightly Windows failures. + * + * Used to find an already-open issue and comment on it instead of filing a + * duplicate, so a week-long breakage is one issue with seven comments. + */ +const WINDOWS_FAILURE_LABEL = 'windows-integ-nightly'; + +/** The nightly (schedule) event. Windows runs unattended here; Linux does not run at all. */ +const IS_SCHEDULE = "github.event_name == 'schedule'"; + +/** + * Windows runs on the nightly, on a manual dispatch, or on a PR that opted in + * via label. + * + * `workflow_dispatch` must be included: neither of the other two triggers can + * be exercised from a branch (a schedule only fires on the default branch, and + * `pull_request_target` reads the workflow from the base branch), so manual + * dispatch is the only way to test a change to these jobs before it merges. + */ +const WINDOWS_REQUESTED_EXPR = [ + IS_SCHEDULE, + "github.event_name == 'workflow_dispatch'", + `contains(github.event.pull_request.labels.*.name, '${WINDOWS_LABEL}')`, +].join(' || '); + /** * Tests that build or run Linux Docker images. * @@ -373,11 +412,21 @@ export class CdkCliIntegTestsWorkflow extends Component { this.workflow.on({ pullRequestTarget: { branches: [], + // 'labeled'/'unlabeled' are NOT in GitHub's default set (which is + // opened/synchronize/reopened), so without them applying the Windows + // opt-in label to an open PR would do nothing until the next push, and + // removing it would leave a stale failed check that blocks the PR. + types: ['opened', 'synchronize', 'reopened', 'labeled', 'unlabeled'], }, // Needs to trigger and report success on merge queue builds as well mergeGroup: {}, // Never hurts to be able to run this manually workflowDispatch: {}, + // Nightly Windows run. Windows is too slow and too flaky-prone to gate + // every PR on, so it runs unattended here and reports failures by filing + // an issue. Deliberately off the hour of the 'upgrade' workflows (00:00) + // and the every-4-hours stale-issue sweep. + schedule: [{ cron: '0 6 * * *' }], }); // Determine the environment dynamically: PRs from the same repo and merge_group // events skip the approval environment, while external PRs require approval. @@ -399,8 +448,10 @@ export class CdkCliIntegTestsWorkflow extends Component { run: `echo ${this.props.approvalEnvironment} > .envname`, }, { - name: 'Skip approval for mergeGroup or PR created from this repo', - if: "${{ github.event_name == 'merge_group' || github.event.pull_request.head.repo.full_name == github.repository }}", + // Scheduled runs are included because there is no human waiting on a + // 06:00 UTC nightly to approve it; without this the run would hang. + name: 'Skip approval for mergeGroup, schedule, or PR created from this repo', + if: `\${{ github.event_name == 'merge_group' || ${IS_SCHEDULE} || github.event.pull_request.head.repo.full_name == github.repository }}`, run: 'echo no-approval > .envname', }, { @@ -454,8 +505,13 @@ export class CdkCliIntegTestsWorkflow extends Component { with: { // IMPORTANT! This must be `head.sha` not `head.ref`, otherwise we // are vulnerable to a TOCTOU attack. - 'ref': '${{ github.event.pull_request.head.sha }}', - 'repository': '${{ github.event.pull_request.head.repo.full_name }}', + // + // The fallbacks cover events with no pull request attached (the + // nightly schedule, and workflow_dispatch), where both of these + // properties are empty. They resolve to the default branch, which + // is what the nightly should be testing. + 'ref': '${{ github.event.pull_request.head.sha || github.sha }}', + 'repository': '${{ github.event.pull_request.head.repo.full_name || github.repository }}', // Need to allow forks, the workflow has been reviewed and getting OIDC credentials is the point // Other credentials are environment protected // @see https://docs.github.com/en/actions/reference/security/securely-using-pull_request_target @@ -598,18 +654,30 @@ export class CdkCliIntegTestsWorkflow extends Component { }], ]; - const testJobs = [ - ...suites.map(([name, jobProps]) => this.addMatrixJob(name, jobProps, { - runsOn: this.props.testRunsOn, - })), - ...(this.props.windowsTestRunsOn - ? suites.map(([name, jobProps]) => this.addMatrixJob(name, jobProps, { - runsOn: this.props.windowsTestRunsOn!, - suffix: '_windows', - windows: true, - })) - : []), - ]; + const linuxJobs = suites.map(([name, jobProps]) => this.addMatrixJob(name, jobProps, { + runsOn: this.props.testRunsOn, + // The nightly exists to cover Windows. Linux already runs on every PR, so + // re-running it unattended would double Atmosphere pool consumption for + // no new signal. + extraCondition: `github.event_name != 'schedule'`, + })); + + const windowsJobs = this.props.windowsTestRunsOn + ? suites.map(([name, jobProps]) => this.addMatrixJob(name, jobProps, { + runsOn: this.props.windowsTestRunsOn!, + suffix: '_windows', + windows: true, + // Only on the nightly, a manual dispatch, or when a PR opts in by label. + extraCondition: `(${WINDOWS_REQUESTED_EXPR})`, + // Windows runs ~4-5x slower than Linux (a smaller runner, and slower + // file IO), which pushes shards past the 1 hour credential ceiling. + // Halve the work per shard to stay under it. + shardScale: 2, + timeoutMinutes: 90, + })) + : []; + + const testJobs = [...linuxJobs, ...windowsJobs]; // Add a job that collates all matrix jobs into a single status // This is required so that we can setup required status checks @@ -633,6 +701,73 @@ export class CdkCliIntegTestsWorkflow extends Component { }, ], }); + + if (windowsJobs.length > 0) { + this.addWindowsFailureReportJob(windowsJobs); + } + } + + /** + * File an issue when the nightly Windows run fails. + * + * Only fires on the schedule. A failure on a label-triggered PR run already + * surfaces as a red check on that PR, so there is nobody to notify. + * + * Reuses an already-open issue rather than filing a duplicate, because a + * breakage that persists for a week would otherwise produce seven identical + * issues. + */ + private addWindowsFailureReportJob(windowsJobs: string[]): void { + this.workflow.addJob('integ_windows_report_failure', { + runsOn: ['ubuntu-latest'], + needs: windowsJobs, + permissions: { + contents: github.workflows.JobPermission.READ, + issues: github.workflows.JobPermission.WRITE, + }, + if: `\${{ always() && ${IS_SCHEDULE} && contains(needs.*.result, 'failure') }}`, + env: { + GH_TOKEN: '${{ secrets.GITHUB_TOKEN }}', + // This job does not check out the repo, so `gh` cannot infer the + // repository from a git remote and needs it passed explicitly. + GH_REPO: '${{ github.repository }}', + // Interpolated here rather than in the `run` body below: CheckGhaExpressions + // rejects `github.repository` (and friends) inside shell steps, because + // attacker-controllable values there are a command injection vector. + // Referenced as a quoted shell variable instead. + RUN_URL: '${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}', + }, + steps: [ + { + name: 'File or update the tracking issue', + run: [ + 'set -euo pipefail', + '', + 'BODY="Nightly Windows integ run failed: $RUN_URL"', + '', + '# --jq \'.[0].number // empty\' yields an empty string when no issue is open,', + '# which distinguishes "nothing found" from a real issue number.', + 'EXISTING=$(gh issue list \\', + ` --label '${WINDOWS_FAILURE_LABEL}' \\`, + ' --state open \\', + ' --limit 1 \\', + ' --json number \\', + ' --jq \'.[0].number // empty\')', + '', + 'if [ -n "$EXISTING" ]; then', + ' echo "Commenting on existing issue #$EXISTING"', + ' gh issue comment "$EXISTING" --body "$BODY"', + 'else', + ' echo "Filing a new issue"', + ' gh issue create \\', + ' --title \'Windows integ nightly is failing\' \\', + ` --label '${WINDOWS_FAILURE_LABEL}' \\`, + ' --body "$BODY"', + 'fi', + ].join('\n'), + }, + ], + }); } private addMatrixJob(testName: string, props: MatrixIntegTestProps, platform: PlatformOptions): string { @@ -643,8 +778,9 @@ export class CdkCliIntegTestsWorkflow extends Component { let shardArg = ''; let logName = `logs${suffix}-\${{ matrix.suite }}-\${{ matrix.node }}`; if (props.domain.shards) { - shard = Array(props.domain.shards).fill(0).map((_, i) => i + 1); - shardArg = ` --shard="\${{ matrix.shard }}/${props.domain.shards}"`; + const shardCount = props.domain.shards * (platform.shardScale ?? 1); + shard = Array(shardCount).fill(0).map((_, i) => i + 1); + shardArg = ` --shard="\${{ matrix.shard }}/${shardCount}"`; logName += '-${{ matrix.shard }}'; } @@ -682,9 +818,14 @@ export class CdkCliIntegTestsWorkflow extends Component { // add extra env at end so it can override ...props.extraEnv, }, + ...platform.timeoutMinutes ? { timeoutMinutes: platform.timeoutMinutes } : {}, // Don't run again on the merge queue, we already got confirmation that it works and the // tests are quite expensive. - if: `github.event_name != 'merge_group' && ${NOT_FLAGGED_EXPR}`, + if: [ + "github.event_name != 'merge_group'", + NOT_FLAGGED_EXPR, + ...platform.extraCondition ? [platform.extraCondition] : [], + ].join(' && '), strategy: { failFast: false, matrix: { @@ -856,4 +997,38 @@ interface PlatformOptions { * @default false */ readonly windows?: boolean; + + /** + * Multiply the declared shard count for this platform. + * + * Slower platforms need smaller shards to keep each job inside the AWS + * session lifetime. The Atmosphere OIDC role has a MaxSessionDuration of 1 + * hour and credentials are obtained immediately before the test step, so a + * suite that runs longer than an hour starts failing Atmosphere calls with + * an expired-token 403 - including the release call that returns the + * allocated environment to the pool. + * + * @default 1 - use the declared shard count as-is + */ + readonly shardScale?: number; + + /** + * Hard cap on job duration. + * + * Bounds the pathological case; without it jobs inherit GitHub's 6 hour + * default. Note this cannot pre-empt the 1 hour credential expiry described + * on `shardScale`: credentials are acquired part-way into the job, at a + * variable offset, so there is no fixed job-level timeout that reliably + * fires before they lapse. + * + * @default - GitHub's default + */ + readonly timeoutMinutes?: number; + + /** + * Additional expression ANDed onto the job's `if` condition. + * + * @default - no additional condition + */ + readonly extraCondition?: string; } From 49b2e00b27ced83706f43f17a1cf8d548ad4df87 Mon Sep 17 00:00:00 2001 From: dgandhi62 Date: Wed, 19 Aug 2026 14:20:48 -0400 Subject: [PATCH 32/34] chore: remove sharing changes --- .github/workflows/integ.yml | 14 +------------- projenrc/cdk-cli-integ-tests.ts | 31 ++++++------------------------- 2 files changed, 7 insertions(+), 38 deletions(-) diff --git a/.github/workflows/integ.yml b/.github/workflows/integ.yml index 8c7a7c11a..aff10f349 100644 --- a/.github/workflows/integ.yml +++ b/.github/workflows/integ.yml @@ -867,7 +867,7 @@ jobs: RELEASE_TAG: latest GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} INTEG_LOGS: logs - run: npx run-suite --shard="${{ matrix.shard }}/24" --use-cli-release=${{ steps.versions.outputs.cli_version }} --framework-version=${{ steps.versions.outputs.lib_version }} ${{ matrix.suite }} + run: npx run-suite --shard="${{ matrix.shard }}/12" --use-cli-release=${{ steps.versions.outputs.cli_version }} --framework-version=${{ steps.versions.outputs.lib_version }} ${{ matrix.suite }} - name: Set workflow summary id: set_workflow_summary if: always() @@ -918,18 +918,6 @@ jobs: - 10 - 11 - 12 - - 13 - - 14 - - 15 - - 16 - - 17 - - 18 - - 19 - - 20 - - 21 - - 22 - - 23 - - 24 integ_toolkit-lib_windows: needs: prepare runs-on: windows-latest diff --git a/projenrc/cdk-cli-integ-tests.ts b/projenrc/cdk-cli-integ-tests.ts index f0c1aeed1..325849900 100644 --- a/projenrc/cdk-cli-integ-tests.ts +++ b/projenrc/cdk-cli-integ-tests.ts @@ -669,10 +669,6 @@ export class CdkCliIntegTestsWorkflow extends Component { windows: true, // Only on the nightly, a manual dispatch, or when a PR opts in by label. extraCondition: `(${WINDOWS_REQUESTED_EXPR})`, - // Windows runs ~4-5x slower than Linux (a smaller runner, and slower - // file IO), which pushes shards past the 1 hour credential ceiling. - // Halve the work per shard to stay under it. - shardScale: 2, timeoutMinutes: 90, })) : []; @@ -778,9 +774,8 @@ export class CdkCliIntegTestsWorkflow extends Component { let shardArg = ''; let logName = `logs${suffix}-\${{ matrix.suite }}-\${{ matrix.node }}`; if (props.domain.shards) { - const shardCount = props.domain.shards * (platform.shardScale ?? 1); - shard = Array(shardCount).fill(0).map((_, i) => i + 1); - shardArg = ` --shard="\${{ matrix.shard }}/${shardCount}"`; + shard = Array(props.domain.shards).fill(0).map((_, i) => i + 1); + shardArg = ` --shard="\${{ matrix.shard }}/${props.domain.shards}"`; logName += '-${{ matrix.shard }}'; } @@ -998,28 +993,14 @@ interface PlatformOptions { */ readonly windows?: boolean; - /** - * Multiply the declared shard count for this platform. - * - * Slower platforms need smaller shards to keep each job inside the AWS - * session lifetime. The Atmosphere OIDC role has a MaxSessionDuration of 1 - * hour and credentials are obtained immediately before the test step, so a - * suite that runs longer than an hour starts failing Atmosphere calls with - * an expired-token 403 - including the release call that returns the - * allocated environment to the pool. - * - * @default 1 - use the declared shard count as-is - */ - readonly shardScale?: number; - /** * Hard cap on job duration. * * Bounds the pathological case; without it jobs inherit GitHub's 6 hour - * default. Note this cannot pre-empt the 1 hour credential expiry described - * on `shardScale`: credentials are acquired part-way into the job, at a - * variable offset, so there is no fixed job-level timeout that reliably - * fires before they lapse. + * default. Note this does not pre-empt AWS session expiry: the Atmosphere + * OIDC role has a MaxSessionDuration of 1 hour and credentials are obtained + * part-way into the job, at a variable offset, so there is no fixed + * job-level timeout that reliably fires before they lapse. * * @default - GitHub's default */ From 9e73610f91bdfe5f37980e4754b20d159deba0ff Mon Sep 17 00:00:00 2001 From: dgandhi62 Date: Wed, 19 Aug 2026 14:56:04 -0400 Subject: [PATCH 33/34] chore: trim comment --- .github/workflows/integ.yml | 3 +- projenrc/cdk-cli-integ-tests.ts | 82 +++++++++++++-------------------- 2 files changed, 33 insertions(+), 52 deletions(-) diff --git a/.github/workflows/integ.yml b/.github/workflows/integ.yml index aff10f349..c9d838317 100644 --- a/.github/workflows/integ.yml +++ b/.github/workflows/integ.yml @@ -1649,8 +1649,7 @@ jobs: BODY="Nightly Windows integ run failed: $RUN_URL" - # --jq '.[0].number // empty' yields an empty string when no issue is open, - # which distinguishes "nothing found" from a real issue number. + # '// empty' yields an empty string when no issue is open, rather than "null". EXISTING=$(gh issue list \ --label 'windows-integ-nightly' \ --state open \ diff --git a/projenrc/cdk-cli-integ-tests.ts b/projenrc/cdk-cli-integ-tests.ts index 325849900..3690b57a2 100644 --- a/projenrc/cdk-cli-integ-tests.ts +++ b/projenrc/cdk-cli-integ-tests.ts @@ -15,35 +15,31 @@ const NOT_FLAGGED_EXPR = "!contains(github.event.pull_request.labels.*.name, 'pr /** * Label that opts a pull request into the Windows integ suites. * - * Windows coverage is expensive and slow, so it does not run on every PR. Add - * this label to a PR that touches platform-sensitive code (paths, subprocess - * spawning, shell quoting) to run it; a failure then blocks the PR like any - * other integ failure. - * - * MUST exist in the repository's label set, otherwise `gh issue create` in the - * nightly failure report will fail. + * Apply it to a PR touching platform-sensitive code (paths, subprocess + * spawning, shell quoting); a failure then blocks the PR like any other integ + * failure. */ const WINDOWS_LABEL = 'pr/test-windows'; /** * Marker label on the issue that tracks nightly Windows failures. * - * Used to find an already-open issue and comment on it instead of filing a - * duplicate, so a week-long breakage is one issue with seven comments. + * MUST exist in the repository's label set, otherwise `gh issue create` in the + * failure report job will fail. */ const WINDOWS_FAILURE_LABEL = 'windows-integ-nightly'; -/** The nightly (schedule) event. Windows runs unattended here; Linux does not run at all. */ +/** The nightly (schedule) event. */ const IS_SCHEDULE = "github.event_name == 'schedule'"; /** * Windows runs on the nightly, on a manual dispatch, or on a PR that opted in * via label. * - * `workflow_dispatch` must be included: neither of the other two triggers can - * be exercised from a branch (a schedule only fires on the default branch, and - * `pull_request_target` reads the workflow from the base branch), so manual - * dispatch is the only way to test a change to these jobs before it merges. + * `workflow_dispatch` is the only one of the three reachable from a branch (a + * schedule fires only on the default branch, and `pull_request_target` reads + * the workflow from the base branch), so it is what makes these jobs testable + * before they merge. */ const WINDOWS_REQUESTED_EXPR = [ IS_SCHEDULE, @@ -412,20 +408,17 @@ export class CdkCliIntegTestsWorkflow extends Component { this.workflow.on({ pullRequestTarget: { branches: [], - // 'labeled'/'unlabeled' are NOT in GitHub's default set (which is - // opened/synchronize/reopened), so without them applying the Windows - // opt-in label to an open PR would do nothing until the next push, and - // removing it would leave a stale failed check that blocks the PR. + // 'labeled'/'unlabeled' are not in GitHub's default set, and without + // them the Windows opt-in label would not take effect (or stop taking + // effect) until the next push. types: ['opened', 'synchronize', 'reopened', 'labeled', 'unlabeled'], }, // Needs to trigger and report success on merge queue builds as well mergeGroup: {}, // Never hurts to be able to run this manually workflowDispatch: {}, - // Nightly Windows run. Windows is too slow and too flaky-prone to gate - // every PR on, so it runs unattended here and reports failures by filing - // an issue. Deliberately off the hour of the 'upgrade' workflows (00:00) - // and the every-4-hours stale-issue sweep. + // Nightly Windows run: too slow to gate every PR on, so it runs + // unattended here and reports failures by filing an issue. schedule: [{ cron: '0 6 * * *' }], }); // Determine the environment dynamically: PRs from the same repo and merge_group @@ -448,8 +441,8 @@ export class CdkCliIntegTestsWorkflow extends Component { run: `echo ${this.props.approvalEnvironment} > .envname`, }, { - // Scheduled runs are included because there is no human waiting on a - // 06:00 UTC nightly to approve it; without this the run would hang. + // The nightly is included because there is nobody waiting to approve + // it; without this it would hang. name: 'Skip approval for mergeGroup, schedule, or PR created from this repo', if: `\${{ github.event_name == 'merge_group' || ${IS_SCHEDULE} || github.event.pull_request.head.repo.full_name == github.repository }}`, run: 'echo no-approval > .envname', @@ -506,10 +499,8 @@ export class CdkCliIntegTestsWorkflow extends Component { // IMPORTANT! This must be `head.sha` not `head.ref`, otherwise we // are vulnerable to a TOCTOU attack. // - // The fallbacks cover events with no pull request attached (the - // nightly schedule, and workflow_dispatch), where both of these - // properties are empty. They resolve to the default branch, which - // is what the nightly should be testing. + // The fallbacks cover events with no pull request attached + // (schedule, workflow_dispatch), and resolve to the default branch. 'ref': '${{ github.event.pull_request.head.sha || github.sha }}', 'repository': '${{ github.event.pull_request.head.repo.full_name || github.repository }}', // Need to allow forks, the workflow has been reviewed and getting OIDC credentials is the point @@ -656,10 +647,9 @@ export class CdkCliIntegTestsWorkflow extends Component { const linuxJobs = suites.map(([name, jobProps]) => this.addMatrixJob(name, jobProps, { runsOn: this.props.testRunsOn, - // The nightly exists to cover Windows. Linux already runs on every PR, so - // re-running it unattended would double Atmosphere pool consumption for - // no new signal. - extraCondition: `github.event_name != 'schedule'`, + // The nightly exists to cover Windows; Linux already runs on every PR, so + // repeating it there would consume Atmosphere environments for no signal. + extraCondition: "github.event_name != 'schedule'", })); const windowsJobs = this.props.windowsTestRunsOn @@ -706,12 +696,9 @@ export class CdkCliIntegTestsWorkflow extends Component { /** * File an issue when the nightly Windows run fails. * - * Only fires on the schedule. A failure on a label-triggered PR run already - * surfaces as a red check on that PR, so there is nobody to notify. - * - * Reuses an already-open issue rather than filing a duplicate, because a - * breakage that persists for a week would otherwise produce seven identical - * issues. + * Schedule-only: a failure on a label-triggered PR run already surfaces as a + * red check there. Comments on an already-open issue rather than filing a + * duplicate for every night of a persistent breakage. */ private addWindowsFailureReportJob(windowsJobs: string[]): void { this.workflow.addJob('integ_windows_report_failure', { @@ -727,10 +714,9 @@ export class CdkCliIntegTestsWorkflow extends Component { // This job does not check out the repo, so `gh` cannot infer the // repository from a git remote and needs it passed explicitly. GH_REPO: '${{ github.repository }}', - // Interpolated here rather than in the `run` body below: CheckGhaExpressions - // rejects `github.repository` (and friends) inside shell steps, because - // attacker-controllable values there are a command injection vector. - // Referenced as a quoted shell variable instead. + // Interpolated here rather than in the `run` body: CheckGhaExpressions + // rejects `github.*` inside shell steps as an injection vector, so the + // step references it as a quoted shell variable instead. RUN_URL: '${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}', }, steps: [ @@ -741,8 +727,7 @@ export class CdkCliIntegTestsWorkflow extends Component { '', 'BODY="Nightly Windows integ run failed: $RUN_URL"', '', - '# --jq \'.[0].number // empty\' yields an empty string when no issue is open,', - '# which distinguishes "nothing found" from a real issue number.', + '# \'// empty\' yields an empty string when no issue is open, rather than "null".', 'EXISTING=$(gh issue list \\', ` --label '${WINDOWS_FAILURE_LABEL}' \\`, ' --state open \\', @@ -994,13 +979,10 @@ interface PlatformOptions { readonly windows?: boolean; /** - * Hard cap on job duration. + * Hard cap on job duration, instead of GitHub's 6 hour default. * - * Bounds the pathological case; without it jobs inherit GitHub's 6 hour - * default. Note this does not pre-empt AWS session expiry: the Atmosphere - * OIDC role has a MaxSessionDuration of 1 hour and credentials are obtained - * part-way into the job, at a variable offset, so there is no fixed - * job-level timeout that reliably fires before they lapse. + * Note this does not pre-empt AWS session expiry: Atmosphere credentials last + * 1 hour and are obtained part-way into the job, at a variable offset. * * @default - GitHub's default */ From 6d6001b17e9433d1c8df5051a593731356626ebf Mon Sep 17 00:00:00 2001 From: dgandhi62 Date: Thu, 20 Aug 2026 11:41:47 -0400 Subject: [PATCH 34/34] chore: add timing --- .../@aws-cdk-testing/cli-integ/lib/index.ts | 1 + .../@aws-cdk-testing/cli-integ/lib/shell.ts | 6 ++ .../@aws-cdk-testing/cli-integ/lib/timing.ts | 69 ++++++++++++++ .../cli-integ/lib/with-cdk-app.ts | 78 +++++++++------ .../cli-integ/lib/with-sam.ts | 5 +- .../cli-integ/lib/with-temporary-directory.ts | 7 +- .../cli-integ/test/shell.test.ts | 26 +++++ .../cli-integ/test/timing.test.ts | 95 +++++++++++++++++++ .../init-typescript-app.integtest.ts | 4 +- 9 files changed, 256 insertions(+), 35 deletions(-) create mode 100644 packages/@aws-cdk-testing/cli-integ/lib/timing.ts create mode 100644 packages/@aws-cdk-testing/cli-integ/test/shell.test.ts create mode 100644 packages/@aws-cdk-testing/cli-integ/test/timing.test.ts diff --git a/packages/@aws-cdk-testing/cli-integ/lib/index.ts b/packages/@aws-cdk-testing/cli-integ/lib/index.ts index a00964d5d..f8aa1da9c 100644 --- a/packages/@aws-cdk-testing/cli-integ/lib/index.ts +++ b/packages/@aws-cdk-testing/cli-integ/lib/index.ts @@ -5,6 +5,7 @@ export * from './memoize'; export * from './resource-pool'; export * from './with-sam'; export * from './shell'; +export * from './timing'; export * from './with-aws'; export * from './with-cdk-app'; export * from './with-packages'; diff --git a/packages/@aws-cdk-testing/cli-integ/lib/shell.ts b/packages/@aws-cdk-testing/cli-integ/lib/shell.ts index b1d2cee66..a2cdcf046 100644 --- a/packages/@aws-cdk-testing/cli-integ/lib/shell.ts +++ b/packages/@aws-cdk-testing/cli-integ/lib/shell.ts @@ -4,6 +4,7 @@ import * as os from 'os'; import * as path from 'path'; import type { TestContext } from './integ-test'; import { Process } from './process'; +import { formatDuration } from './timing'; import type { TemporaryDirectoryContext } from './with-temporary-directory'; /** @@ -36,6 +37,7 @@ export async function shell(command: string[], options: ShellOptions = {}): Prom // We choose the lazy one. const spawnOptions = { ...options, env } as any; + const startTime = Date.now(); const child = tty ? Process.spawnTTY(command[0], command.slice(1), spawnOptions) : Process.spawn(command[0], command.slice(1), spawnOptions); @@ -109,6 +111,10 @@ export async function shell(command: string[], options: ShellOptions = {}): Prom child.onError(reject); child.onExit(code => { + // Always report how long the command took, on success and on failure alike, + // so slow steps can be found by reading the log. Pairs with the '💻' line above. + writeToOutputs(`⏱️ ${formatDuration(Date.now() - startTime)} ${command.join(' ')}\n`); + const stderrOutput = Buffer.concat(stderr).toString('utf-8'); const stdoutOutput = Buffer.concat(stdout).toString('utf-8'); const out = (options.onlyStderr ? stderrOutput : stdoutOutput + stderrOutput).trim(); diff --git a/packages/@aws-cdk-testing/cli-integ/lib/timing.ts b/packages/@aws-cdk-testing/cli-integ/lib/timing.ts new file mode 100644 index 000000000..76a2ddf79 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/lib/timing.ts @@ -0,0 +1,69 @@ +/** + * Duration reporting for the integ test log. + * + * Every command run through `shell()` reports how long it took. Operations that + * do real work *without* spawning a process (recursive copies, directory walks, + * waiting on another worker) need to report themselves, or they show up as + * unexplained gaps in a test's duration. This matters most on Windows, where + * writing and deleting many small files is far slower than on Linux. + * + * Both helpers use the same '💻' / '⏱️' line shape as `shell()`, so a single + * search for '⏱️' in a test log finds every measured step. + */ + +/** + * Render a duration in milliseconds for humans reading the test log. + * + * Uses the same units as the per-test durations in the GitHub Actions summary + * ('2m17s', '9.4s', '386ms'), so the two can be compared without converting. + */ +export function formatDuration(millis: number): string { + if (millis < 1_000) { + return `${millis}ms`; + } + + if (millis < 60_000) { + return `${(millis / 1_000).toFixed(1)}s`; + } + + // Round to whole seconds before splitting, so we can never render '1m60s' + const totalSeconds = Math.round(millis / 1_000); + return `${Math.floor(totalSeconds / 60)}m${totalSeconds % 60}s`; +} + +/** + * Run an async operation, reporting how long it took. + * + * The duration is reported whether the operation succeeds or throws, so a step + * that spent two minutes before failing is still visible in the log. + */ +export async function timed( + description: string, + output: NodeJS.WritableStream | undefined, + block: () => Promise, +): Promise { + output?.write(`💻 ${description}\n`); + const startTime = Date.now(); + try { + return await block(); + } finally { + output?.write(`⏱️ ${formatDuration(Date.now() - startTime)} ${description}\n`); + } +} + +/** + * `timed`, for operations that are synchronous. + */ +export function timedSync( + description: string, + output: NodeJS.WritableStream | undefined, + block: () => A, +): A { + output?.write(`💻 ${description}\n`); + const startTime = Date.now(); + try { + return block(); + } finally { + output?.write(`⏱️ ${formatDuration(Date.now() - startTime)} ${description}\n`); + } +} diff --git a/packages/@aws-cdk-testing/cli-integ/lib/with-cdk-app.ts b/packages/@aws-cdk-testing/cli-integ/lib/with-cdk-app.ts index 4f46dda3c..5016bc427 100644 --- a/packages/@aws-cdk-testing/cli-integ/lib/with-cdk-app.ts +++ b/packages/@aws-cdk-testing/cli-integ/lib/with-cdk-app.ts @@ -15,6 +15,7 @@ import { testSource } from './package-sources/subprocess'; import { RESOURCES_DIR } from './resources'; import type { ShellOptions } from './shell'; import { shell, ShellHelper, rimraf } from './shell'; +import { timed, timedSync } from './timing'; import type { AwsContext, AwsContextOptions } from './with-aws'; import { atmosphereEnabled, withAws } from './with-aws'; import { withTimeout } from './with-timeout'; @@ -280,10 +281,13 @@ export interface CdkDestroyCliOptions extends CdkCliOptions { * Prepare a target dir byreplicating a source directory */ export async function cloneDirectory(source: string, target: string, output?: NodeJS.WritableStream) { - output?.write(`Cloning ${source} into ${target}\n`); - await fs.promises.rm(target, { recursive: true, force: true }); - await fs.promises.mkdir(target, { recursive: true }); - await fs.promises.cp(source, target, { recursive: true }); + // Recursive copy of many small files, which is slow on Windows. Timed because + // it used to be three `shell()` commands and would otherwise vanish from the log. + await timed(`clone ${source} -> ${target}`, output, async () => { + await fs.promises.rm(target, { recursive: true, force: true }); + await fs.promises.mkdir(target, { recursive: true }); + await fs.promises.cp(source, target, { recursive: true }); + }); } interface CommonCdkBootstrapCommandOptions { @@ -876,7 +880,7 @@ export class TestFixture extends ShellHelper { // If the tests completed successfully, happily delete the fixture // (otherwise leave it for humans to inspect) if (success) { - const cleaned = rimraf(this.integTestDir); + const cleaned = timedSync(`clean up ${this.integTestDir}`, this.output, () => rimraf(this.integTestDir)); if (!cleaned) { console.error(`Failed to clean up ${this.integTestDir} due to permissions issues (Docker running as root?)`); } @@ -1095,37 +1099,49 @@ async function sharedPackageSetInstall(fixture: TestFixture, packages: Record deadline) { - throw new Error(`Timed out waiting for shared install of ${JSON.stringify(packages)} in '${sharedDir}'`); - } + // Timed as a whole: a worker that loses the lock race blocks here until the + // winner finishes installing, which counts against this test's duration even + // though this test isn't doing the work. The inner 'npm install' line only + // appears for the worker that actually won. + return timed(`shared package set ${hash}`, fixture.output, async () => { + const deadline = Date.now() + 30 * 60 * 1000; + let announcedWait = false; - try { - fs.mkdirSync(lockDir); - } catch { - // Another worker is installing; wait for it to finish. - await sleep(5_000); - continue; - } - - try { + while (true) { if (fs.existsSync(completeMarker)) { return nodeModules; } - fixture.log(`Installing shared package set into '${sharedDir}'`); - fs.mkdirSync(sharedDir, { recursive: true }); - fs.copyFileSync(path.join(fixture.integTestDir, 'package.json'), path.join(sharedDir, 'package.json')); - await npmInstallWithRetry(fixture, sharedDir); - fs.writeFileSync(completeMarker, ''); - return nodeModules; - } finally { - fs.rmdirSync(lockDir); + if (Date.now() > deadline) { + throw new Error(`Timed out waiting for shared install of ${JSON.stringify(packages)} in '${sharedDir}'`); + } + + try { + fs.mkdirSync(lockDir); + } catch { + // Another worker is installing; wait for it to finish. + if (!announcedWait) { + fixture.log(`Waiting for another worker to finish installing '${sharedDir}'`); + announcedWait = true; + } + await sleep(5_000); + continue; + } + + try { + if (fs.existsSync(completeMarker)) { + return nodeModules; + } + fixture.log(`Installing shared package set into '${sharedDir}'`); + fs.mkdirSync(sharedDir, { recursive: true }); + fs.copyFileSync(path.join(fixture.integTestDir, 'package.json'), path.join(sharedDir, 'package.json')); + await npmInstallWithRetry(fixture, sharedDir); + fs.writeFileSync(completeMarker, ''); + return nodeModules; + } finally { + fs.rmdirSync(lockDir); + } } - } + }); } async function npmInstallWithRetry(fixture: TestFixture, cwd: string) { diff --git a/packages/@aws-cdk-testing/cli-integ/lib/with-sam.ts b/packages/@aws-cdk-testing/cli-integ/lib/with-sam.ts index e3350a84c..7664085d1 100644 --- a/packages/@aws-cdk-testing/cli-integ/lib/with-sam.ts +++ b/packages/@aws-cdk-testing/cli-integ/lib/with-sam.ts @@ -5,6 +5,7 @@ import type { TestContext } from './integ-test'; import { RESOURCES_DIR } from './resources'; import type { ShellOptions } from './shell'; import { rimraf } from './shell'; +import { formatDuration, timedSync } from './timing'; import type { AwsContext } from './with-aws'; import { withAws } from './with-aws'; import { @@ -150,7 +151,7 @@ export class SamIntegrationTestFixture extends TestFixture { // If the tests completed successfully, happily delete the fixture // (otherwise leave it for humans to inspect) if (success) { - const cleaned = rimraf(this.integTestDir); + const cleaned = timedSync(`clean up ${this.integTestDir}`, this.output, () => rimraf(this.integTestDir)); if (!cleaned) { // eslint-disable-next-line no-console console.error(`Failed to clean up ${this.integTestDir} due to permissions issues (Docker running as root?)`); @@ -188,6 +189,7 @@ export async function shellWithAction( const env = options.env ?? (options.modEnv ? { ...process.env, ...options.modEnv } : undefined); + const startTime = Date.now(); const child = child_process.spawn(command.join(' '), [], { ...options, env, @@ -256,6 +258,7 @@ export async function shellWithAction( // Wait for 'exit' instead of close, don't care about reading the streams all the way to the end child.once('exit', (code, signal) => { + writeToOutputs(`⏱️ ${formatDuration(Date.now() - startTime)} ${command.join(' ')}\n`); writeToOutputs(`Subprocess has exited with code ${code}, signal ${signal}\n`); const output = (Buffer.concat(stdout).toString('utf-8') + Buffer.concat(stderr).toString('utf-8')).trim(); if (code == null || code === 0 || options.allowErrExit) { diff --git a/packages/@aws-cdk-testing/cli-integ/lib/with-temporary-directory.ts b/packages/@aws-cdk-testing/cli-integ/lib/with-temporary-directory.ts index f88b4b033..9ded3c153 100644 --- a/packages/@aws-cdk-testing/cli-integ/lib/with-temporary-directory.ts +++ b/packages/@aws-cdk-testing/cli-integ/lib/with-temporary-directory.ts @@ -3,6 +3,7 @@ import * as os from 'os'; import * as path from 'path'; import type { TestContext } from './integ-test'; import { rimraf } from './shell'; +import { timedSync } from './timing'; export interface TemporaryDirectoryContext { readonly integTestDir: string; @@ -24,7 +25,11 @@ export function withTemporaryDirectory(block: (context: A if (process.env.SKIP_CLEANUP) { context.log(`Left test directory in '${integTestDir}' ($SKIP_CLEANUP)\n`); } else { - rimraf(integTestDir); + // Recursive delete of the whole test tree, which for the init suites holds + // 'node_modules' / '.venv' / NuGet 'obj' / Maven 'target'. Deleting many + // small files is slow on Windows, and this runs inside the test's measured + // window, so it has to be visible in the log. + timedSync(`clean up ${integTestDir}`, context.output, () => rimraf(integTestDir)); } } catch (e) { context.log(`Left test directory in '${integTestDir}'\n`); diff --git a/packages/@aws-cdk-testing/cli-integ/test/shell.test.ts b/packages/@aws-cdk-testing/cli-integ/test/shell.test.ts new file mode 100644 index 000000000..4d949733e --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/test/shell.test.ts @@ -0,0 +1,26 @@ +import { MemoryStream } from '../lib/corking'; +import { shell } from '../lib/shell'; + +describe('shell command timing', () => { + // Commands are run through a shell, so they must not contain shell metacharacters + const TIMING_LINE = /⏱️\s+\d+(\.\d+)?(ms|s)\s/; + + test('reports the duration of a successful command', async () => { + const output = new MemoryStream(); + + await shell([process.execPath, '--version'], { outputs: [output] }); + + // The '💻' line announces the command, the '⏱️' line reports how long it took + expect(output.toString()).toMatch(TIMING_LINE); + }); + + test('reports the duration of a failing command too', async () => { + const output = new MemoryStream(); + + await expect( + shell([process.execPath, '--definitely-not-a-node-flag'], { outputs: [output] }), + ).rejects.toThrow(/exited with error code/); + + expect(output.toString()).toMatch(TIMING_LINE); + }); +}); diff --git a/packages/@aws-cdk-testing/cli-integ/test/timing.test.ts b/packages/@aws-cdk-testing/cli-integ/test/timing.test.ts new file mode 100644 index 000000000..c7dd46eaf --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/test/timing.test.ts @@ -0,0 +1,95 @@ +import { MemoryStream } from '../lib/corking'; +import { formatDuration, timed, timedSync } from '../lib/timing'; + +describe('formatDuration', () => { + test.each([ + [0, '0ms'], + [1, '1ms'], + [386, '386ms'], + [999, '999ms'], + ])('renders %dms as milliseconds: %s', (millis, expected) => { + expect(formatDuration(millis)).toEqual(expected); + }); + + test.each([ + [1_000, '1.0s'], + [9_386, '9.4s'], + [29_961, '30.0s'], + [59_949, '59.9s'], + ])('renders %dms as seconds with one decimal: %s', (millis, expected) => { + expect(formatDuration(millis)).toEqual(expected); + }); + + test.each([ + [60_000, '1m0s'], + [137_000, '2m17s'], + [3_600_000, '60m0s'], + ])('renders %dms as minutes and seconds: %s', (millis, expected) => { + expect(formatDuration(millis)).toEqual(expected); + }); + + test('rolls up to the next minute instead of rendering 60 seconds', () => { + // 119_700ms rounds to 120s, which must not come out as '1m60s' + expect(formatDuration(119_700)).toEqual('2m0s'); + }); +}); + +// Matches '⏱️ 57ms some description' +const TIMING_LINE = /⏱️\s+\d+(\.\d+)?(ms|s)\s/; + +describe('timed', () => { + test('announces the operation and reports its duration', async () => { + const output = new MemoryStream(); + + await timed('do the thing', output, async () => { + }); + + expect(output.toString()).toMatch(/💻 do the thing/); + expect(output.toString()).toMatch(TIMING_LINE); + }); + + test('returns the value produced by the block', async () => { + expect(await timed('compute', undefined, async () => 42)).toEqual(42); + }); + + test('reports the duration even when the block throws', async () => { + const output = new MemoryStream(); + + await expect(timed('failing thing', output, async () => { + throw new Error('boom'); + })).rejects.toThrow('boom'); + + expect(output.toString()).toMatch(TIMING_LINE); + }); + + test('tolerates a missing output stream', async () => { + await expect(timed('no output', undefined, async () => { + })).resolves.toBeUndefined(); + }); +}); + +describe('timedSync', () => { + test('announces the operation and reports its duration', () => { + const output = new MemoryStream(); + + timedSync('do the sync thing', output, () => { + }); + + expect(output.toString()).toMatch(/💻 do the sync thing/); + expect(output.toString()).toMatch(TIMING_LINE); + }); + + test('returns the value produced by the block', () => { + expect(timedSync('compute', undefined, () => 42)).toEqual(42); + }); + + test('reports the duration even when the block throws', () => { + const output = new MemoryStream(); + + expect(() => timedSync('failing thing', output, () => { + throw new Error('boom'); + })).toThrow('boom'); + + expect(output.toString()).toMatch(TIMING_LINE); + }); +}); diff --git a/packages/@aws-cdk-testing/cli-integ/tests/init-typescript-app/init-typescript-app.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/init-typescript-app/init-typescript-app.integtest.ts index 5f8796336..c34aeffc0 100644 --- a/packages/@aws-cdk-testing/cli-integ/tests/init-typescript-app/init-typescript-app.integtest.ts +++ b/packages/@aws-cdk-testing/cli-integ/tests/init-typescript-app/init-typescript-app.integtest.ts @@ -1,7 +1,7 @@ import { promises as fs } from 'fs'; import * as path from 'path'; import type { TemporaryDirectoryContext } from '../../lib'; -import { integTest, withTemporaryDirectory, ShellHelper, withPackages } from '../../lib'; +import { integTest, withTemporaryDirectory, ShellHelper, withPackages, timed } from '../../lib'; import { typescriptVersionsSync, typescriptVersionsYoungerThanDaysSync } from '../../lib/npm'; ['app', 'sample-app'].forEach(template => { @@ -55,7 +55,7 @@ TYPESCRIPT_VERSIONS.forEach(tsVersion => { await shell.shell(['npm', 'ls']); // this will fail if we have unmet peer dependencies // We just removed the 'jest' dependency so remove the tests as well because they won't compile - await fs.rm(path.join(context.integTestDir, 'test'), { recursive: true, force: true }); + await timed('remove test/', context.output, () => fs.rm(path.join(context.integTestDir, 'test'), { recursive: true, force: true })); await shell.shell(['npm', 'run', 'build']); await shell.shell(['cdk', 'synth']);