Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
52cba19
feat(cli-integ): also run integ test suites on Windows
iankhou Jul 29, 2026
a1f15d2
fix(cli-integ): start Verdaccio via node interpreter for Windows
iankhou Jul 29, 2026
c3d3280
fix(cli-integ): make test harness work on Windows
iankhou Jul 30, 2026
cb60a8e
fix(cli-integ): spawn npm through node in typescript version lookups
iankhou Jul 30, 2026
c0bade5
fix(cli-integ): search all stage assemblies for nested template
iankhou Jul 30, 2026
a27886e
fix(cli-integ): Windows fixes for docker login env expansion and pyth…
iankhou Jul 30, 2026
e8adbe3
fix(cli-integ): disable wincred credential helper for docker login on…
iankhou Jul 30, 2026
4da22c7
fix(cli-integ): raise init test timeouts to 5 minutes
iankhou Jul 30, 2026
38039f3
fix(cli-integ): write ECR auth directly to docker config on Windows
iankhou Jul 30, 2026
f0cf15a
feat(cli-integ): exclude work directories from Defender on Windows ru…
iankhou Jul 30, 2026
688abe5
fix(cli-integ): skip Linux docker tests on Windows, raise typescript-…
iankhou Jul 30, 2026
deef751
feat(cli-integ): replace Defender exclusion with a Dev Drive for TEMP
iankhou Jul 30, 2026
bce87c2
feat(cli-integ): extend Dev Drive to npm cache, grow VHDX to 40GB
iankhou Jul 30, 2026
ea35192
fix(cli-integ): skip four more Linux-image tests on Windows
iankhou Jul 30, 2026
7dc181d
fix(cli-integ): request 4-hour OIDC session for Windows integ jobs
iankhou Jul 30, 2026
430217e
revert(cli-integ): back to 1-hour OIDC session for Windows integ jobs
iankhou Jul 30, 2026
2d37e70
chore(cli-integ): remove session duration comment
iankhou Jul 30, 2026
6a88a4a
fix(cli-integ): spawn TTY processes through the shell on Windows
iankhou Jul 30, 2026
77e1b59
fix(cli-integ): make cdk watch tests work on Windows
iankhou Jul 30, 2026
8d96157
fix(cli-integ): deliver Windows skip list via file, add two docker tests
iankhou Jul 30, 2026
82bc735
fix(cli-integ): share one npm install across tests on Windows
iankhou Jul 31, 2026
1036149
fix(cli-integ): widen ConPTY terminal so long prompts match
iankhou Jul 31, 2026
1363823
fix(cli-integ): skip sam local test on Windows
iankhou Jul 31, 2026
b339d3e
fix(cli-integ): match prompts in ConPTY screen-buffer output
iankhou Jul 31, 2026
92ae558
fix(cli-integ): start Verdaccio without pm2, poll for readiness
iankhou Aug 1, 2026
76eccbe
feat(cli-integ): share a weekly npm cache across integ jobs
iankhou Aug 1, 2026
2fb8afb
Revert "feat(cli-integ): share a weekly npm cache across integ jobs"
iankhou Aug 1, 2026
85c2ec9
fix(cli-integ): ship Verdaccio to test jobs as a prebuilt bundle
iankhou Aug 1, 2026
12a4983
fix(cli-integ): pin bundled Verdaccio to 6.8 for Node 20 jobs
iankhou Aug 1, 2026
360fc94
fix(cli-integ): fall back to npm install when Verdaccio bundle is mis…
dgandhi62 Aug 14, 2026
26fe26d
feat(cli-integ): run Windows integ suites nightly and on label, not e…
dgandhi62 Aug 18, 2026
49b2e00
chore: remove sharing changes
dgandhi62 Aug 19, 2026
9e73610
chore: trim comment
dgandhi62 Aug 19, 2026
6d6001b
chore: add timing
dgandhi62 Aug 20, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
989 changes: 956 additions & 33 deletions .github/workflows/integ.yml

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions .projenrc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1769,6 +1769,10 @@ new CdkCliIntegTestsWorkflow(repo, {
testEnvironment: TEST_ENVIRONMENT,
buildRunsOn: POWERFUL_RUNNER,
testRunsOn: POWERFUL_RUNNER,
// Also run the integ suites on Windows to catch platform-specific
// regressions (paths, subprocess spawning). Uses the free standard runner
// for now; switch to a larger runner label once one is provisioned.
windowsTestRunsOn: 'windows-latest',

allowUpstreamVersions: [
// cloud-assembly-schema gets referenced under multiple versions
Expand Down
1 change: 1 addition & 0 deletions packages/@aws-cdk-testing/cli-integ/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export * from './memoize';
export * from './resource-pool';
export * from './with-sam';
export * from './shell';
export * from './timing';
export * from './with-aws';
export * from './with-cdk-app';
export * from './with-packages';
Expand Down
5 changes: 3 additions & 2 deletions packages/@aws-cdk-testing/cli-integ/lib/npm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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('.'))));
Expand All @@ -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<string, string> = JSON.parse(stdout);

const cutoffDate = new Date(Date.now() - (days * 24 * 3600 * 1000));
Expand Down
16 changes: 14 additions & 2 deletions packages/@aws-cdk-testing/cli-integ/lib/process.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

/**
Expand Down
68 changes: 65 additions & 3 deletions packages/@aws-cdk-testing/cli-integ/lib/shell.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import * as os from 'os';
import * as path from 'path';
import type { TestContext } from './integ-test';
import { Process } from './process';
import { formatDuration } from './timing';
import type { TemporaryDirectoryContext } from './with-temporary-directory';

/**
Expand Down Expand Up @@ -36,6 +37,7 @@ export async function shell(command: string[], options: ShellOptions = {}): Prom
// We choose the lazy one.
const spawnOptions = { ...options, env } as any;

const startTime = Date.now();
const child = tty
? Process.spawnTTY(command[0], command.slice(1), spawnOptions)
: Process.spawn(command[0], command.slice(1), spawnOptions);
Expand Down Expand Up @@ -109,6 +111,10 @@ export async function shell(command: string[], options: ShellOptions = {}): Prom
child.onError(reject);

child.onExit(code => {
// Always report how long the command took, on success and on failure alike,
// so slow steps can be found by reading the log. Pairs with the '💻' line above.
writeToOutputs(`⏱️ ${formatDuration(Date.now() - startTime)} ${command.join(' ')}\n`);

const stderrOutput = Buffer.concat(stderr).toString('utf-8');
const stdoutOutput = Buffer.concat(stdout).toString('utf-8');
const out = (options.onlyStderr ? stderrOutput : stdoutOutput + stderrOutput).trim();
Expand Down Expand Up @@ -282,7 +288,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)) {
Expand Down Expand Up @@ -310,13 +331,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);
}

/**
Expand All @@ -339,7 +360,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
Expand All @@ -351,10 +393,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, '');
}
69 changes: 69 additions & 0 deletions packages/@aws-cdk-testing/cli-integ/lib/timing.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/**
* Duration reporting for the integ test log.
*
* Every command run through `shell()` reports how long it took. Operations that
* do real work *without* spawning a process (recursive copies, directory walks,
* waiting on another worker) need to report themselves, or they show up as
* unexplained gaps in a test's duration. This matters most on Windows, where
* writing and deleting many small files is far slower than on Linux.
*
* Both helpers use the same '💻' / '⏱️' line shape as `shell()`, so a single
* search for '⏱️' in a test log finds every measured step.
*/

/**
* Render a duration in milliseconds for humans reading the test log.
*
* Uses the same units as the per-test durations in the GitHub Actions summary
* ('2m17s', '9.4s', '386ms'), so the two can be compared without converting.
*/
export function formatDuration(millis: number): string {
if (millis < 1_000) {
return `${millis}ms`;
}

if (millis < 60_000) {
return `${(millis / 1_000).toFixed(1)}s`;
}

// Round to whole seconds before splitting, so we can never render '1m60s'
const totalSeconds = Math.round(millis / 1_000);
return `${Math.floor(totalSeconds / 60)}m${totalSeconds % 60}s`;
}

/**
* Run an async operation, reporting how long it took.
*
* The duration is reported whether the operation succeeds or throws, so a step
* that spent two minutes before failing is still visible in the log.
*/
export async function timed<A>(
description: string,
output: NodeJS.WritableStream | undefined,
block: () => Promise<A>,
): Promise<A> {
output?.write(`💻 ${description}\n`);
const startTime = Date.now();
try {
return await block();
} finally {
output?.write(`⏱️ ${formatDuration(Date.now() - startTime)} ${description}\n`);
}
}

/**
* `timed`, for operations that are synchronous.
*/
export function timedSync<A>(
description: string,
output: NodeJS.WritableStream | undefined,
block: () => A,
): A {
output?.write(`💻 ${description}\n`);
const startTime = Date.now();
try {
return block();
} finally {
output?.write(`⏱️ ${formatDuration(Date.now() - startTime)} ${description}\n`);
}
}
Loading
Loading