Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
36 changes: 36 additions & 0 deletions packages/@aws-cdk-testing/cli-integ/lib/npm.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -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 `<bin> --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 `<pkg>@<range>` 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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, string> = {
'aws-cdk': 'cdk',
'cdk-assets': 'cdk-assets',
};

function cliBinName(packageName: string): string {
return CLI_BIN_NAMES[packageName] ?? packageName;
}

export class RunnerCliNpmSource implements IRunnerSource<ITestCliSource> {
public readonly sourceDescription: string;

Expand All @@ -16,13 +31,22 @@ export class RunnerCliNpmSource implements IRunnerSource<ITestCliSource> {
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() {
Expand Down
Loading