diff --git a/packages/@aws-cdk-testing/cli-integ/lib/npm.ts b/packages/@aws-cdk-testing/cli-integ/lib/npm.ts index 857240a0e..ea5d11a90 100644 --- a/packages/@aws-cdk-testing/cli-integ/lib/npm.ts +++ b/packages/@aws-cdk-testing/cli-integ/lib/npm.ts @@ -1,5 +1,6 @@ // eslint-disable-next-line no-restricted-imports -- cli-integ is a test harness that spawns processes to exercise the CLI as a user would; it is test infrastructure, not shipped runtime. import { spawnSync } from 'child_process'; +import * as path from 'path'; import * as semver from 'semver'; import { shell } from './shell'; @@ -26,6 +27,41 @@ export async function npmMostRecentMatching(packageName: string, range: string) return output[output.length - 1]; } +/** + * Verify that the CLI binary installed under `installRoot` is actually runnable. + * + * `npm install` recording a version (see `npmQueryInstalledVersion`) does not + * guarantee the package's `bin` landed in `node_modules/.bin` or that it runs. + * During an npm registry degradation an install can be incomplete: the version + * is recorded but the `cdk` binary is missing, which only surfaces much later + * as `cdk: not found` (exit 127) from inside a test's `cdk synth` — pointing + * investigators at CDK/synth instead of at the install. + * + * Invoking ` --version` through the same install root the tests will use + * turns that downstream failure into a clear, install-time diagnosis. + * + * @param binName - the executable to run (e.g. `cdk`) + * @param installRoot - the directory that contains `node_modules/.bin` + * @param installSpec - the `@` that was installed, for the error message + */ +export async function verifyCliRunnable(binName: string, installRoot: string, installSpec: string) { + const binPath = path.join(installRoot, 'node_modules', '.bin', binName); + try { + await shell([binPath, '--version'], { + cwd: installRoot, + show: 'error', + captureStderr: true, + outputs: [process.stderr], + }); + } catch (e) { + throw new Error( + `CLI install verification failed: '${binName} --version' did not run after installing ${installSpec}. ` + + 'This usually indicates an incomplete or degraded npm install rather than a CDK defect. ' + + `(underlying error: ${e})`, + ); + } +} + export async function npmQueryInstalledVersion(packageName: string, dir: string) { const reportStr = await shell(['node', require.resolve('npm'), 'list', '--json', '--depth', '0', packageName], { cwd: dir, diff --git a/packages/@aws-cdk-testing/cli-integ/lib/package-sources/cli-npm-source.ts b/packages/@aws-cdk-testing/cli-integ/lib/package-sources/cli-npm-source.ts index 8c548b9a8..cb8da3c37 100644 --- a/packages/@aws-cdk-testing/cli-integ/lib/package-sources/cli-npm-source.ts +++ b/packages/@aws-cdk-testing/cli-integ/lib/package-sources/cli-npm-source.ts @@ -2,9 +2,24 @@ import * as os from 'os'; import * as path from 'path'; import * as fs from 'fs-extra'; import type { IRunnerSource, ITestCliSource, IPreparedRunnerSource } from './source'; -import { npmQueryInstalledVersion } from '../npm'; +import { npmQueryInstalledVersion, verifyCliRunnable } from '../npm'; import { addToShellPath, rimraf, shell } from '../shell'; +/** + * The executable that a given CLI package installs into `node_modules/.bin`. + * + * npm names the bin after the `bin` key in the package's `package.json`, which + * does not always equal the package name (e.g. `aws-cdk` installs `cdk`). + */ +const CLI_BIN_NAMES: Record = { + 'aws-cdk': 'cdk', + 'cdk-assets': 'cdk-assets', +}; + +function cliBinName(packageName: string): string { + return CLI_BIN_NAMES[packageName] ?? packageName; +} + export class RunnerCliNpmSource implements IRunnerSource { public readonly sourceDescription: string; @@ -16,13 +31,22 @@ export class RunnerCliNpmSource implements IRunnerSource { const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'tmpcdk')); fs.mkdirSync(tempDir, { recursive: true }); - await shell(['node', require.resolve('npm'), 'install', `${this.packageName}@${this.range}`], { + const installSpec = `${this.packageName}@${this.range}`; + + await shell(['node', require.resolve('npm'), 'install', installSpec], { cwd: tempDir, show: 'error', outputs: [process.stderr], }); + const installedVersion = await npmQueryInstalledVersion(this.packageName, tempDir); + // Fail fast, with an npm-attributable message, if the bin didn't land or + // isn't runnable. Recording a version does not prove the CLI is usable; + // an incomplete install otherwise surfaces much later as `cdk: not found` + // (exit 127) from inside a test's `cdk synth`, blaming the wrong component. + await verifyCliRunnable(cliBinName(this.packageName), tempDir, installSpec); + return { version: installedVersion, async dispose() {