From 94f1ae482d52846534cf2c3542f4201ac34c374c Mon Sep 17 00:00:00 2001 From: dgandhi62 Date: Wed, 19 Aug 2026 16:48:39 -0400 Subject: [PATCH 01/10] fix(cli-integ): make the integ test harness work on Windows The integ test harness and several suites assume a POSIX environment, so they cannot run on a Windows runner. This makes them platform-portable without changing behaviour on Linux. - spawn TTY processes through the shell, and widen the ConPTY terminal so long prompts are not wrapped before they are matched - match prompts against ConPTY screen-buffer output - spawn npm through the node interpreter rather than relying on bin shims - share one npm install across tests, which dominates runtime on Windows - fix path handling in the watch tests and search all stage assemblies for the nested template - give the init suites an explicit 5 minute timeout; they previously ran on the 60s suite default, which is not enough for Maven, NuGet or Go module downloads No Windows jobs run yet; enabling those is a follow-up. --- .../@aws-cdk-testing/cli-integ/lib/npm.ts | 5 +- .../@aws-cdk-testing/cli-integ/lib/process.ts | 16 ++- .../@aws-cdk-testing/cli-integ/lib/shell.ts | 62 +++++++++++- .../cli-integ/lib/with-cdk-app.ts | 99 +++++++++++++++++-- ...nerating-and-loading-assembly.integtest.ts | 29 +++++- ...isk-contain-metadata-resource.integtest.ts | 31 ++++-- ...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 ++++- .../init-csharp/init-csharp.integtest.ts | 2 +- .../init-fsharp/init-fsharp.integtest.ts | 2 +- .../tests/init-go/init-go.integtest.ts | 2 +- .../tests/init-java/init-java.integtest.ts | 2 +- .../init-javascript.integtest.ts | 4 +- .../init-python/init-python.integtest.ts | 12 ++- .../init-typescript-app.integtest.ts | 6 +- .../init-typescript-lib.integtest.ts | 2 +- ...use-lib-as-bundled-dependency.integtest.ts | 2 +- 19 files changed, 270 insertions(+), 52 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)); diff --git a/packages/@aws-cdk-testing/cli-integ/lib/process.ts b/packages/@aws-cdk-testing/cli-integ/lib/process.ts index 9b64ee585..5e08f966e 100644 --- a/packages/@aws-cdk-testing/cli-integ/lib/process.ts +++ b/packages/@aws-cdk-testing/cli-integ/lib/process.ts @@ -48,11 +48,23 @@ 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', + // 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(process); + return new PtyProcess(ptyProcess); } /** diff --git a/packages/@aws-cdk-testing/cli-integ/lib/shell.ts b/packages/@aws-cdk-testing/cli-integ/lib/shell.ts index 436ee7633..b1d2cee66 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)) { @@ -310,13 +325,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); } /** @@ -339,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 @@ -351,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, ''); +} 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..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'; @@ -279,9 +280,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 { @@ -505,15 +507,33 @@ 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.'); } + 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({ 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', '${ECR_PASSWORD}', @@ -1045,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; @@ -1054,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' )) { 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..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 @@ -1,3 +1,5 @@ +import { promises as fs } from 'fs'; +import * as path from 'path'; import { integTest, withDefaultFixture } from '../../../lib'; integTest( @@ -7,17 +9,34 @@ 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', - ]); + // 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(); }), ); +/** + * 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 ${root}`); + } + return path.join(match.parentPath, match.name); +} + +async function readMatchingFile(root: string, pattern: RegExp): Promise { + return fs.readFile(await findMatchingFile(root, pattern), 'utf-8'); +} + 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 } 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 4e4a89b22..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 @@ -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 }); - }))); + })), 300_000); }); 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..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 @@ -55,11 +55,11 @@ 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']); - }))); + })), 300_000); }); async function removeDevDependencies(context: TemporaryDirectoryContext) { 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 a8f331f5b361ed119adba4efc07e1cbb7960450c Mon Sep 17 00:00:00 2001 From: dgandhi62 Date: Fri, 21 Aug 2026 11:47:22 -0400 Subject: [PATCH 02/10] feat(cli-integ): share one npm install across tests on all platforms The shared install added for Windows applies just as well everywhere: every test asks for the same handful of packages at the same resolved versions, so installing per test is duplicated work. On Linux that shows up as many concurrent `npm install` processes, which is a known source of ECONNRESET failures, and as slow installs in the CodeBuild canary runs. - drop the win32 gate, and pick the symlink type per platform ('junction' on Windows, where a 'dir' symlink needs elevation; ignored on POSIX) - keep per-test installs when REPO_ROOT rewrites a package to a local directory: the cache is keyed on the requested package set, and a directory path does not change when its contents are rebuilt, so sharing there would serve stale code. No package installed here is currently a workspace of this repo, so this is a guard, not a fix - coordinate through the existing XpMutex instead of a hand-rolled lock directory. It reclaims a lock whose owner has died by checking pid liveness, rather than waiting out a timeout: previously a worker killed mid-install left a lock nothing would release, so every other test on the machine waited out the 30 minute deadline and failed Keying on the package set is safe because `requestedVersion()` always resolves to an exact version before it reaches the installer. Addresses review feedback on the shared-install block. --- .../@aws-cdk-testing/cli-integ/lib/shell.ts | 2 +- .../cli-integ/lib/with-cdk-app.ts | 111 +++++++++++------- 2 files changed, 70 insertions(+), 43 deletions(-) diff --git a/packages/@aws-cdk-testing/cli-integ/lib/shell.ts b/packages/@aws-cdk-testing/cli-integ/lib/shell.ts index b1d2cee66..910e9e727 100644 --- a/packages/@aws-cdk-testing/cli-integ/lib/shell.ts +++ b/packages/@aws-cdk-testing/cli-integ/lib/shell.ts @@ -286,7 +286,7 @@ export function rimraf(fsPath: string): boolean { // 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). + // shared 'node_modules'). if (stat.isSymbolicLink()) { try { fs.unlinkSync(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 4f46dda3c..c1bab6edf 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 @@ -18,6 +18,7 @@ import { shell, ShellHelper, rimraf } from './shell'; import type { AwsContext, AwsContextOptions } from './with-aws'; import { atmosphereEnabled, withAws } from './with-aws'; import { withTimeout } from './with-timeout'; +import { XpMutexPool } from './xpmutex'; import { findYarnPackages } from './yarn'; export const DEFAULT_TEST_TIMEOUT_S = 20 * 60; @@ -1031,9 +1032,13 @@ function hasJsonFlag(args: string[]): boolean { /** * Install the given NPM packages, identified by their names and versions * - * Works by writing the packages to a `package.json` file, and - * then running NPM7's "install" on it. The use of NPM7 will automatically - * install required peerDependencies. + * Works by writing the packages to a `package.json` file, and then running NPM7's + * "install" on it. The use of NPM7 will automatically install required + * peerDependencies. + * + * The install itself is shared: because every test asks for the same handful of + * packages at the same resolved versions, they are installed once per machine and + * linked into each test directory. See `sharedPackageSetInstall`. * * If we're running in REPO mode and we find the package in the set of local * packages in the repository, we'll write the directory name to `package.json` @@ -1047,6 +1052,8 @@ function hasJsonFlag(args: string[]): boolean { * for Node's dependency lookup mechanism). */ export async function installNpmPackages(fixture: TestFixture, packages: Record) { + let hasLocalPackages = false; + if (process.env.REPO_ROOT) { const monoRepo = await findYarnPackages(process.env.REPO_ROOT); @@ -1054,6 +1061,7 @@ export async function installNpmPackages(fixture: TestFixture, packages: Record< for (const key of Object.keys(packages)) { if (key in monoRepo) { packages[key] = monoRepo[key]; + hasLocalPackages = true; } } } @@ -1065,26 +1073,52 @@ 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'); + if (hasLocalPackages) { + // A local package is referenced by directory, so the package set no longer + // identifies its own contents: rebuilding changes what is on disk without + // changing the requested version. Install per test, so that the dev cycle + // of 'rebuild, rerun the test' keeps working. + await npmInstallWithRetry(fixture, fixture.integTestDir); return; } - await npmInstallWithRetry(fixture, fixture.integTestDir); + // Every test installs the same small set of packages, and `aws-cdk-lib` alone is + // tens of thousands of files, so installing per test is pure duplicated work: it + // is very slow on Windows (minutes instead of seconds), and on every platform it + // means many concurrent `npm install` processes, which is a source of ECONNRESET + // failures. Install each distinct package set once per machine and link it into + // the test directory instead. + const sharedNodeModules = await sharedPackageSetInstall(fixture, packages); + fs.symlinkSync( + sharedNodeModules, + path.join(fixture.integTestDir, 'node_modules'), + // Ignored on POSIX. On Windows a 'junction' works for unprivileged users, + // where a 'dir' symlink needs elevation. + process.platform === 'win32' ? 'junction' : 'dir', + ); } +/** + * Mutex pool guarding the shared installs, created on first use. + * + * Constructing a pool starts an `fs.watch`, so don't do it for test runs that + * never install anything. + */ +let installMutexPool: XpMutexPool | undefined; + /** * 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. + * Concurrent callers (jest workers are separate processes) coordinate through a + * cross-process mutex: whoever holds it installs, and everyone else waits and then + * finds the completion marker already there. A worker that dies while installing + * holds a lock nobody would ever release, so `XpMutex` reclaims it once the owning + * pid is gone. + * + * The shared directory is keyed on the requested package set. Those versions are + * always fully resolved by the time they get here (see `requestedVersion()` on the + * library sources), so the key identifies the contents and the directory can be + * reused across runs on the same machine. * * @returns the path of the installed `node_modules` directory. */ @@ -1092,39 +1126,32 @@ async function sharedPackageSetInstall(fixture: TestFixture, packages: Record 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); - } + 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 { + await lock.release(); } } From 3450fda585ada81be3675425ef9cc96635543ec2 Mon Sep 17 00:00:00 2001 From: dgandhi62 Date: Fri, 21 Aug 2026 12:07:34 -0400 Subject: [PATCH 03/10] fix: change placement of lock file to resolve test expectations --- .../@aws-cdk-testing/cli-integ/lib/with-cdk-app.ts | 10 ++++++++++ 1 file changed, 10 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 c1bab6edf..f3c46db71 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 @@ -1096,6 +1096,16 @@ export async function installNpmPackages(fixture: TestFixture, packages: Record< // where a 'dir' symlink needs elevation. process.platform === 'win32' ? 'junction' : 'dir', ); + + // `npm` writes the lock file next to the `package.json` it installed, which is now + // the shared directory, so copy it back into the test directory. Constructs that + // bundle (`NodejsFunction`) find their project root by searching upwards from the + // app for a lock file, and bundle-mount that directory into Docker; without a lock + // file here the search escapes the test directory and synth fails. + fs.copyFileSync( + path.join(sharedNodeModules, '..', 'package-lock.json'), + path.join(fixture.integTestDir, 'package-lock.json'), + ); } /** From dcf9a97a87ca8c91e70670cc5c5ca7c80cf854d0 Mon Sep 17 00:00:00 2001 From: dgandhi62 Date: Fri, 21 Aug 2026 12:45:10 -0400 Subject: [PATCH 04/10] feat: update timeouts --- .../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 | 2 +- .../cli-integ/tests/init-python/init-python.integtest.ts | 2 +- .../init-typescript-app/init-typescript-app.integtest.ts | 4 ++-- .../init-typescript-lib/init-typescript-lib.integtest.ts | 2 +- .../use-lib-as-bundled-dependency.integtest.ts | 2 +- 9 files changed, 10 insertions(+), 10 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 3af10939d..617b2c3f2 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); + })), 180_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 d7d96e032..81ac0d32c 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); + })), 240_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 8f501d1c4..8890b481e 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); + })), 240_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 45d5dead0..60acc7f42 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); + })), 180_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 38b9e2014..359deda46 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 @@ -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); +})), 360_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 075671f78..61807c144 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); + })), 240_000); }); 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..30789fb76 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']); - })), 600_000); + })), 300_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); + })), 180_000); }); async function removeDevDependencies(context: TemporaryDirectoryContext) { 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 2f73b06ed..9152eddc6 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); +})), 180_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 9b22aab91..26c35b646 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); +})), 180_000); From b0ac0bb55c3c8e1754860ce25ced4c4e0a85edb5 Mon Sep 17 00:00:00 2001 From: dgandhi62 Date: Fri, 21 Aug 2026 12:47:22 -0400 Subject: [PATCH 05/10] feat: update timeouts --- .../tests/init-javascript/init-javascript.integtest.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 359deda46..499fa7e55 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); + })), 180_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']); -})), 360_000); +})), 180_000); From 6ac620493ef66ea3b3a497de0bc2aa274bce9e6a Mon Sep 17 00:00:00 2001 From: dgandhi62 Date: Fri, 21 Aug 2026 14:48:52 -0400 Subject: [PATCH 06/10] fix(cli-integ): shared-install mutex crashes on Windows with EPERM The cross-process mutex guarding the shared npm install represents a lock as a file: acquire by exclusively creating it, release by unlinking it. This assumes POSIX deletion semantics, where the only "can't create" signal is EEXIST and the only "can't read" signal is ENOENT. Windows differs. A lock file that another process still has open, or that was just unlinked, enters a "delete pending" state: it lingers in the directory but open()/read() against it fail with EPERM/EACCES. Under the heavy startup contention the shared install creates (every jest worker races for the same lock), tryAcquire() hit EPERM, fell into the `code !== 'EEXIST'` branch, and rethrew a fatal error. On the Windows integ runner this took down every test in the suite with an identical 'EPERM: operation not permitted, open ...cdk-integ-shared-install...mutex'. Treat EPERM/EACCES the same as the POSIX signals: on exclusive create they mean "held or mid-transition, back off and retry" (like EEXIST); on read they mean "not readable, treat as gone" (like ENOENT). Add a short sleep before retrying so a persistent delete-pending window does not busy-spin. POSIX behavior is unchanged: EPERM does not occur on this path there, so the new branches are inert on Linux and macOS. Also bump the init-typescript-app integ test timeouts (300s->600s, 180s->300s) to account for the slower Windows runners. --- .../@aws-cdk-testing/cli-integ/lib/xpmutex.ts | 36 +++++++++++++++++-- .../cli-integ/test/xpmutex.test.ts | 31 ++++++++++++++++ .../init-typescript-app.integtest.ts | 4 +-- 3 files changed, 66 insertions(+), 5 deletions(-) diff --git a/packages/@aws-cdk-testing/cli-integ/lib/xpmutex.ts b/packages/@aws-cdk-testing/cli-integ/lib/xpmutex.ts index 372395272..2787c1010 100644 --- a/packages/@aws-cdk-testing/cli-integ/lib/xpmutex.ts +++ b/packages/@aws-cdk-testing/cli-integ/lib/xpmutex.ts @@ -2,6 +2,30 @@ import { watch, promises as fs, mkdirSync } from 'fs'; import * as os from 'os'; import * as path from 'path'; +/** + * Error codes that mean "the lock file is currently held or in the middle of a + * transition", i.e. we could not create it right now and should back off. + * + * On POSIX the only such signal is `EEXIST` (the exclusive create found the + * file already there). On Windows a file that another process still has open, + * or that was just unlinked, enters a "delete pending" state: it lingers in the + * directory but `open()` against it fails with `EPERM`/`EACCES` instead of + * `EEXIST`. Under contention (many workers racing for the same lock) this is a + * routine, transient condition, not a fatal error, so we treat it the same as + * `EEXIST` and retry. + */ +const CONTENDED_CODES = ['EEXIST', 'EPERM', 'EACCES']; + +/** + * Error codes that mean "the lock file is not readable right now", which we + * treat as "it isn't there" and retry. + * + * `ENOENT` is the file being gone; on Windows `EPERM`/`EACCES` additionally + * cover the delete-pending window, where the name still exists but cannot be + * opened for reading. + */ +const UNREADABLE_CODES = ['ENOENT', 'EPERM', 'EACCES']; + export class XpMutexPool { public static fromDirectory(directory: string) { mkdirSync(directory, { recursive: true }); @@ -96,7 +120,9 @@ export class XpMutex { try { return await this.writePidFile('wx'); // Fails if the file already exists } catch (e: any) { - if (e.code !== 'EEXIST') { + // EEXIST: the lock is held. On Windows a delete-pending lock file + // surfaces as EPERM/EACCES instead; treat those the same way and retry. + if (!CONTENDED_CODES.includes(e.code)) { throw e; } } @@ -104,7 +130,9 @@ export class XpMutex { // File already exists. Read the contents, see if it's an existent PID (if so, the lock is taken) const ownerPid = await this.readPidFile(); if (ownerPid === undefined) { - // File got deleted just now, maybe we can acquire it again + // File got deleted just now (or is mid-transition on Windows). Pause + // briefly so we don't spin on a delete-pending file, then try again. + await randomSleep(10); continue; } if (processExists(ownerPid)) { @@ -164,7 +192,9 @@ export class XpMutex { try { contents = await fs.readFile(this.fileName, { encoding: 'utf-8' }); } catch (e: any) { - if (e.code === 'ENOENT') { + // ENOENT: the file is gone. On Windows a delete-pending file is still + // named but unreadable (EPERM/EACCES); treat it as gone and retry. + if (UNREADABLE_CODES.includes(e.code)) { return undefined; } throw e; diff --git a/packages/@aws-cdk-testing/cli-integ/test/xpmutex.test.ts b/packages/@aws-cdk-testing/cli-integ/test/xpmutex.test.ts index 73e0d9140..7d10f49a9 100644 --- a/packages/@aws-cdk-testing/cli-integ/test/xpmutex.test.ts +++ b/packages/@aws-cdk-testing/cli-integ/test/xpmutex.test.ts @@ -1,3 +1,4 @@ +import { promises as fs } from 'fs'; import { XpMutexPool } from '../lib/xpmutex'; const POOL = XpMutexPool.fromName('test-pool'); @@ -30,6 +31,36 @@ test('acquire waits', async () => { await secondProcess; }); +test('a Windows delete-pending EPERM on create is treated as contention, not a fatal error', async () => { + // On Windows, creating the lock file can transiently fail with EPERM while a + // just-unlinked file is in "delete pending" state. The mutex must swallow + // that and retry rather than throwing it up to the caller (which is what + // took down the shared-install lock on the Windows integ runner). + const mux = POOL.mutex('windowsEperm'); + + const realOpen = fs.open.bind(fs); + let epermInjected = 0; + const spy = jest.spyOn(fs, 'open').mockImplementation((async (...args: any[]) => { + // Fail the first exclusive-create attempt exactly once, as Windows would. + if (args[1] === 'wx' && epermInjected < 1) { + epermInjected++; + const e: any = new Error("EPERM: operation not permitted, open ''"); + e.code = 'EPERM'; + throw e; + } + return realOpen(...(args as Parameters)); + }) as unknown as typeof fs.open); + + try { + // Would reject with EPERM before the fix; now it retries and succeeds. + const lock = await mux.acquire(); + expect(epermInjected).toBe(1); + await lock.release(); + } finally { + spy.mockRestore(); + } +}); + /** * Poll for some condition every 10ms */ 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 30789fb76..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']); - })), 180_000); + })), 300_000); }); async function removeDevDependencies(context: TemporaryDirectoryContext) { From 518cfad2323d8c25b725daab358be7791dade1ea Mon Sep 17 00:00:00 2001 From: dgandhi62 Date: Fri, 21 Aug 2026 15:23:39 -0400 Subject: [PATCH 07/10] chore: update timeouts --- .../tests/init-typescript-lib/init-typescript-lib.integtest.ts | 2 +- .../use-lib-as-bundled-dependency.integtest.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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 9152eddc6..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']); -})), 180_000); +})), 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 26c35b646..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']); -})), 180_000); +})), 300_000); From fd2b2ba63de73d98df0d07e8eb2b9bd4b945eeae Mon Sep 17 00:00:00 2001 From: dgandhi62 Date: Fri, 21 Aug 2026 16:34:23 -0400 Subject: [PATCH 08/10] chore: add retries --- .../cli-integ/lib/integ-test.ts | 30 ++++++++- .../cli-integ/test/integ-test.test.ts | 64 +++++++++++++++++++ 2 files changed, 92 insertions(+), 2 deletions(-) create mode 100644 packages/@aws-cdk-testing/cli-integ/test/integ-test.test.ts diff --git a/packages/@aws-cdk-testing/cli-integ/lib/integ-test.ts b/packages/@aws-cdk-testing/cli-integ/lib/integ-test.ts index 49832f245..aec8386d7 100644 --- a/packages/@aws-cdk-testing/cli-integ/lib/integ-test.ts +++ b/packages/@aws-cdk-testing/cli-integ/lib/integ-test.ts @@ -229,12 +229,38 @@ function slugify(x: string) { return x.replace(/[^a-zA-Z0-9_,]+/g, '-'); } -async function atomicWrite(fileName: string, contents: string) { +/** + * Write a file by writing to a temp file and renaming it into place. + * + * On POSIX the final rename atomically replaces any existing destination, and + * concurrent writers of the same target harmlessly clobber each other. On + * Windows, replacing a destination that another process currently has open (or + * that is in a "delete pending" state from a concurrent replace) fails with + * EPERM/EACCES. Multiple test workers rewrite shared log files (notably + * `0-header.md`) at once, so ride out that transient window by retrying the + * rename a handful of times before giving up. + */ +export async function atomicWrite(fileName: string, contents: string) { await fs.promises.mkdir(path.dirname(fileName), { recursive: true }); const tmp = `${fileName}.${process.pid}`; await fs.promises.writeFile(tmp, contents); - await fs.promises.rename(tmp, fileName); + + const RENAME_RETRYABLE = ['EPERM', 'EACCES']; + const maxAttempts = 10; + for (let attempt = 1; ; attempt++) { + try { + await fs.promises.rename(tmp, fileName); + return; + } catch (e: any) { + if (!RENAME_RETRYABLE.includes(e.code) || attempt >= maxAttempts) { + // Final failure: don't leave the temp file behind as litter. + await fs.promises.rm(tmp, { force: true }).catch(() => undefined); + throw e; + } + await new Promise(ok => setTimeout(ok, Math.floor(Math.random() * 20) + 5)); + } + } } function readSkipFile(filePath?: string): string[] { diff --git a/packages/@aws-cdk-testing/cli-integ/test/integ-test.test.ts b/packages/@aws-cdk-testing/cli-integ/test/integ-test.test.ts new file mode 100644 index 000000000..7369ec7c7 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/test/integ-test.test.ts @@ -0,0 +1,64 @@ +import { promises as fs } from 'fs'; +import * as os from 'os'; +import * as path from 'path'; +import { atomicWrite } from '../lib/integ-test'; + +let dir: string; + +beforeEach(async () => { + dir = await fs.mkdtemp(path.join(os.tmpdir(), 'atomic-write-test-')); +}); + +afterEach(async () => { + await fs.rm(dir, { recursive: true, force: true }); +}); + +test('atomicWrite writes the file contents', async () => { + const target = path.join(dir, 'out.txt'); + await atomicWrite(target, 'hello'); + expect(await fs.readFile(target, 'utf-8')).toBe('hello'); +}); + +test('atomicWrite retries a Windows-style EPERM on rename and still writes the file', async () => { + // On Windows, renaming onto a destination another worker has open fails with + // EPERM. When several workers rewrite the same shared log file concurrently + // this is transient, so atomicWrite must retry rather than propagate it (which + // is what failed the migrate test on the Windows integ runner). + const target = path.join(dir, 'shared.md'); + + const realRename = fs.rename.bind(fs); + let epermInjected = 0; + const spy = jest.spyOn(fs, 'rename').mockImplementation((async (...args: any[]) => { + // Fail the first rename attempt once, as Windows would under contention. + if (epermInjected < 1) { + epermInjected++; + const e: any = new Error('EPERM: operation not permitted, rename'); + e.code = 'EPERM'; + throw e; + } + return realRename(...(args as Parameters)); + }) as unknown as typeof fs.rename); + + try { + await atomicWrite(target, 'body'); // would throw before the fix + expect(epermInjected).toBe(1); + expect(await fs.readFile(target, 'utf-8')).toBe('body'); + } finally { + spy.mockRestore(); + } +}); + +test('atomicWrite rethrows a non-retryable error', async () => { + const target = path.join(dir, 'nope.txt'); + const spy = jest.spyOn(fs, 'rename').mockImplementation((async () => { + const e: any = new Error('ENOSPC: no space left on device'); + e.code = 'ENOSPC'; + throw e; + }) as unknown as typeof fs.rename); + + try { + await expect(atomicWrite(target, 'x')).rejects.toThrow('ENOSPC'); + } finally { + spy.mockRestore(); + } +}); From 901af6ffd037092603a875b20c6b7b055550beda Mon Sep 17 00:00:00 2001 From: dgandhi62 Date: Wed, 19 Aug 2026 16:49:24 -0400 Subject: [PATCH 09/10] feat(cli-integ): run the integ suites on Windows, nightly and on label Adds Windows instances of the five integ suites and decides when they run. Windows is slow and flaky-prone, so it does not gate every PR: - nightly at 06:00 UTC, where it runs unattended and reports a failure by filing (or commenting on) a tracking issue - on a PR that opts in with the 'pr/test-windows' label - on a manual workflow_dispatch, which is the only one of the three reachable from a branch Linux is excluded from the nightly, since it already runs on every PR. Supporting workflow changes: a Dev Drive for TEMP and the npm cache, since the suites are dominated by small-file writes; bash as the default shell so the shared step scripts run unchanged under Git Bash; a Windows skip list for tests that need Linux Docker images; approval bypass and checkout fallbacks for events that carry no pull request. Requires the 'pr/test-windows' and 'windows-integ-nightly' labels to exist in the repository. --- .github/workflows/integ.yml | 989 ++++++++++++++++++++++++++++++-- .projenrc.ts | 4 + projenrc/cdk-cli-integ-tests.ts | 354 +++++++++++- 3 files changed, 1290 insertions(+), 57 deletions(-) diff --git a/.github/workflows/integ.yml b/.github/workflows/integ.yml index e6a059ff5..c9d838317 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 @@ -74,24 +82,672 @@ jobs: env: 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) + tar czf .projen/verdaccio-bundle.tgz -C /tmp/verdaccio-bundle node_modules - name: Upload artifact id: build-artifact uses: actions/upload-artifact@v7 with: - name: build-artifact - path: packages/**/dist/js/*.tgz + name: build-artifact + path: packages/**/dist/js/*.tgz + overwrite: true + - name: Upload artifact + id: script-artifact + uses: actions/upload-artifact@v7 + with: + name: script-artifact + path: |- + .projen/*.sh + .projen/verdaccio-bundle.tgz + overwrite: true + include-hidden-files: true + integ_cli: + 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 --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: Upload artifact - id: script-artifact + - 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 + 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: script-artifact - path: .projen/*.sh + name: ${{ steps.artifactid.outputs.slug }} + path: logs/ overwrite: true - include-hidden-files: true - integ_cli: + - 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: aws-cdk_ubuntu-latest_16-core + runs-on: windows-latest permissions: contents: read id-token: write @@ -101,8 +757,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 @@ -183,7 +879,7 @@ jobs: id: artifactid if: always() env: - INPUT: logs-${{ matrix.suite }}-${{ matrix.node }}-${{ matrix.shard }} + 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" @@ -201,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: @@ -221,9 +918,9 @@ jobs: - 10 - 11 - 12 - integ_toolkit-lib: + integ_toolkit-lib_windows: needs: prepare - runs-on: aws-cdk_ubuntu-latest_16-core + runs-on: windows-latest permissions: contents: read id-token: write @@ -233,8 +930,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 @@ -315,7 +1052,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" @@ -333,6 +1070,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: @@ -343,9 +1081,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 @@ -355,8 +1093,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 @@ -437,7 +1215,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" @@ -455,6 +1233,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: @@ -462,9 +1241,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 @@ -474,8 +1253,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 @@ -556,7 +1375,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" @@ -574,6 +1393,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: @@ -595,9 +1415,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 @@ -607,8 +1427,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 @@ -689,7 +1549,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" @@ -707,6 +1567,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: @@ -721,6 +1582,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() @@ -740,7 +1606,64 @@ 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" + + # '// empty' yields an empty string when no issue is open, rather than "null". + 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.ts b/.projenrc.ts index d599b69bc..cdfe794e1 100644 --- a/.projenrc.ts +++ b/.projenrc.ts @@ -1770,6 +1770,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..3690b57a2 100644 --- a/projenrc/cdk-cli-integ-tests.ts +++ b/projenrc/cdk-cli-integ-tests.ts @@ -12,6 +12,74 @@ 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. + * + * 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. + * + * 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. */ +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` 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, + "github.event_name == 'workflow_dispatch'", + `contains(github.event.pull_request.labels.*.name, '${WINDOWS_LABEL}')`, +].join(' || '); + +/** + * 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', + // 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', + '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 { return { name: 'Setup Node.js', @@ -130,6 +198,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 * @@ -289,11 +368,33 @@ export class CdkCliIntegTestsWorkflow extends Component { committed: false, lines: [ '#!/bin/bash', - 'npm install -g verdaccio pm2', + // 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. + // + // 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', + '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`, - 'pm2 start verdaccio -- --config $HOME/.config/verdaccio/config.yaml', - 'sleep 5', // Wait for Verdaccio to start + // 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', + ' 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', @@ -307,11 +408,18 @@ export class CdkCliIntegTestsWorkflow extends Component { this.workflow.on({ pullRequestTarget: { branches: [], + // '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: 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 // events skip the approval environment, while external PRs require approval. @@ -333,8 +441,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 }}", + // 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', }, { @@ -388,8 +498,11 @@ 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 + // (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 // Other credentials are environment protected // @see https://docs.github.com/en/actions/reference/security/securely-using-pull_request_target @@ -426,6 +539,26 @@ 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', + // 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'), + }, github.WorkflowSteps.uploadArtifact({ id: 'build-artifact', with: { @@ -438,7 +571,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, }, @@ -449,36 +585,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,17 +634,37 @@ 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 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 + // repeating it there would consume Atmosphere environments for no 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})`, + 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 // and if we ever change the test matrix, we don't need to update @@ -530,14 +687,77 @@ export class CdkCliIntegTestsWorkflow extends Component { }, ], }); + + if (windowsJobs.length > 0) { + this.addWindowsFailureReportJob(windowsJobs); + } + } + + /** + * File an issue when the nightly Windows run fails. + * + * 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', { + 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: 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: [ + { + name: 'File or update the tracking issue', + run: [ + 'set -euo pipefail', + '', + 'BODY="Nightly Windows integ run failed: $RUN_URL"', + '', + '# \'// empty\' yields an empty string when no issue is open, rather than "null".', + '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): 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 +766,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. @@ -562,12 +789,23 @@ export class CdkCliIntegTestsWorkflow extends Component { // assumptions about the availability of source packages. IS_CANARY: 'true', CI: 'true', + ...platform.windows ? { + // 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, }, + ...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: { @@ -581,6 +819,33 @@ export class CdkCliIntegTestsWorkflow extends Component { }, }, steps: [ + ...platform.windows ? [{ + // 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 and npm cache', + shell: 'powershell', + 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', + // 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'), + }, { + 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: { artifactIds: [`\${{needs.${this.JOB_PREPARE}.outputs.packagesArtifact}}`], @@ -689,3 +954,44 @@ 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; + + /** + * Whether this job runs on a Windows runner. + * + * Adds Windows-specific setup steps. + * + * @default false + */ + readonly windows?: boolean; + + /** + * Hard cap on job duration, instead of GitHub's 6 hour default. + * + * 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 + */ + readonly timeoutMinutes?: number; + + /** + * Additional expression ANDed onto the job's `if` condition. + * + * @default - no additional condition + */ + readonly extraCondition?: string; +} From bb92d256eae8b5399758c913ae7b7ab3358a35fe Mon Sep 17 00:00:00 2001 From: dgandhi62 Date: Thu, 20 Aug 2026 17:19:13 -0400 Subject: [PATCH 10/10] The nightly Windows integ run has nobody watching it, so a failure now files a GitHub issue labelled 'potential-regression', which is already wired up to page the team. Only on the schedule. A PR that opts into the Windows suites via the 'pr/test-windows' label files nothing: the failure is already a red check on the PR, and the label is there so a contributor can try Windows out, not to page anyone. The issue records the commit SHA as well as the run URL. Dependency upgrades merge unattended at 00:00 UTC and the nightly runs at 06:00, so consecutive nightlies do not necessarily test the same commit. The job MUST keep the default GITHUB_TOKEN. Issues created with it do not trigger other workflow runs, which is what stops issue-regression-labeler from stripping 'potential-regression' off an issue whose body has no regression checkbox. A PAT here would silently stop the page. --- .github/workflows/integ.yml | 30 ++++++------------- projenrc/cdk-cli-integ-tests.ts | 52 ++++++++++++++++----------------- 2 files changed, 34 insertions(+), 48 deletions(-) diff --git a/.github/workflows/integ.yml b/.github/workflows/integ.yml index c9d838317..11a68d462 100644 --- a/.github/workflows/integ.yml +++ b/.github/workflows/integ.yml @@ -1640,30 +1640,18 @@ jobs: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} GH_REPO: ${{ github.repository }} RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + RUN_SHA: ${{ github.sha }} 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 + - name: File an issue + id: file_an_issue run: |- set -euo pipefail - BODY="Nightly Windows integ run failed: $RUN_URL" + gh issue create \ + --title 'Windows integ nightly failed' \ + --label 'potential-regression' \ + --body "Nightly Windows integ run failed. - # '// empty' yields an empty string when no issue is open, rather than "null". - 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 + Run: $RUN_URL + Commit: $RUN_SHA" diff --git a/projenrc/cdk-cli-integ-tests.ts b/projenrc/cdk-cli-integ-tests.ts index 3690b57a2..d01087f7a 100644 --- a/projenrc/cdk-cli-integ-tests.ts +++ b/projenrc/cdk-cli-integ-tests.ts @@ -22,12 +22,13 @@ const NOT_FLAGGED_EXPR = "!contains(github.event.pull_request.labels.*.name, 'pr const WINDOWS_LABEL = 'pr/test-windows'; /** - * Marker label on the issue that tracks nightly Windows failures. + * Label applied to the issue filed when the nightly Windows run fails. * - * MUST exist in the repository's label set, otherwise `gh issue create` in the - * failure report job will fail. + * This is the repository's existing regression label, which is already wired up + * to page the team. `issue-regression-labeler` also manages it, so it is + * guaranteed to exist in the repository's label set. */ -const WINDOWS_FAILURE_LABEL = 'windows-integ-nightly'; +const REGRESSION_LABEL = 'potential-regression'; /** The nightly (schedule) event. */ const IS_SCHEDULE = "github.event_name == 'schedule'"; @@ -696,9 +697,9 @@ export class CdkCliIntegTestsWorkflow extends Component { /** * File an issue when the nightly Windows run fails. * - * 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. + * Schedule-only. A label-triggered PR run deliberately does not file an + * issue: the failure is already visible as a red check on the PR, and the + * label exists so a contributor can try Windows out, not to page anyone. */ private addWindowsFailureReportJob(windowsJobs: string[]): void { this.workflow.addJob('integ_windows_report_failure', { @@ -710,6 +711,12 @@ export class CdkCliIntegTestsWorkflow extends Component { }, if: `\${{ always() && ${IS_SCHEDULE} && contains(needs.*.result, 'failure') }}`, env: { + // MUST stay the default GITHUB_TOKEN. Issues created with it do not + // trigger other workflow runs, which is what keeps + // `issue-regression-labeler` from firing: that workflow strips + // 'potential-regression' from any issue whose body lacks the regression + // checkbox, and would silently undo the label we set here. Switching + // this to a PAT would stop the page from ever going out. 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. @@ -718,33 +725,24 @@ export class CdkCliIntegTestsWorkflow extends Component { // 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 }}', + // Dependency upgrades merge unattended overnight, so the nightly does + // not necessarily run against the same commit as the night before. + // Record what it did run against. + RUN_SHA: '${{ github.sha }}', }, steps: [ { - name: 'File or update the tracking issue', + name: 'File an issue', run: [ 'set -euo pipefail', '', - 'BODY="Nightly Windows integ run failed: $RUN_URL"', + 'gh issue create \\', + ' --title \'Windows integ nightly failed\' \\', + ` --label '${REGRESSION_LABEL}' \\`, + ' --body "Nightly Windows integ run failed.', '', - '# \'// empty\' yields an empty string when no issue is open, rather than "null".', - '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', + 'Run: $RUN_URL', + 'Commit: $RUN_SHA"', ].join('\n'), }, ],