Skip to content

feat: add a preinstalled source option for cli, cdk-assets, framework - #2067

Open
dgandhi62 wants to merge 4 commits into
mainfrom
integ-offline-npm
Open

dgandhi62 wants to merge 4 commits into
mainfrom
integ-offline-npm

Conversation

@dgandhi62

@dgandhi62 dgandhi62 commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

This pr adds a consistent "preinstalled" option across run-suite's 4 test dependencies (aws-cdk, cdk-assets, aws-cdk-lib, @aws-cdk/toolkit-lib). This feature helps so that run-suite can run in an isolated environment where every dependency has already been installed ahead of time.

Currently, we resolve its four deps through package sources. Based on the selected source, resolution uses npm install or npm view against the public registry. @aws-cdk/toolkit-lib already had a RunnerLibraryPreinstalledSource, but it was only reachable through an auto-detect fallback, and the other three dependencies had no equivalent at all.

I'm also adding 2 unit tests for this use case. The first one drops a fake package into the target directory and directs the source to find and use it. The second one asserts on an error thrown if no package is found.

Furthermore, to verify, the below command passes with 0 internet

./bin/run-suite cli-integ-tests \
  --cli-preinstalled --cdk-assets-preinstalled \
  --framework-preinstalled --toolkit-lib-preinstalled \
  --test '____no_such_test_pattern_xyz____' \
  --passWithNoTests --maxWorkers=1

We kick off the test run with the pre-installed sources but don't run any tests. This is because the tests themselves have npm installs in them and I wanted to separate concerns.

Checklist

  • This change contains a major version upgrade for a dependency and I confirm all breaking changes are addressed
    • Release notes for the new version:

By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license

@github-actions

Copy link
Copy Markdown
Contributor

Dependency Review

✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.

Scanned Files

None

@maintainer-for-aws

maintainer-for-aws Bot commented Oct 21, 2026 •

Copy link
Copy Markdown

Automated review

A maintainer will still review this — treat the notes below as a starting point.

📌 Commit d0217f5

This PR extends the CLI integration-test harness (run-suite) so all four test dependencies — the CLI, cdk-assets, framework, and toolkit-lib — can be sourced from an already-installed location via new --*-preinstalled flags, enabling offline/air-gapped runs. It adds a new RunnerCliPreinstalledSource, wires the flags and mutual-exclusion through the existing UniqueOption wrapper, registers the subprocess deserialization constructors, adds a unit test, and documents the flags. The change is test-infrastructure only, with no customer-facing CDK code affected. Deserialization wiring, mutual exclusion of version vs. preinstalled flags, and the .bin path resolution all check out. The two items below are a maintainability suggestion about duplicated resolution logic and an optional cleanup-residue note in the new test.

🔴 0 blocking · 🟡 1 recommended · ⚪ 1 optional

Files with findings (2)
File Findings
packages/@aws-cdk-testing/cli-integ/lib/package-sources/cli-preinstalled-source.ts 🟡 1
packages/@aws-cdk-testing/cli-integ/test/cli-preinstalled-source.test.ts ⚪ 1

Generated automatically. React 👍 or 👎 to tell us whether this review helped, so we can improve these reviews.

@maintainer-for-aws maintainer-for-aws Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See the review summary comment for the overview; the notes below are inline.

Comment on lines +18 to +37

public async runnerPrepare(): Promise<IPreparedRunnerSource<ITestCliSource>> {
// Pretend to be in the test directory and resolve the package
const searchPath = path.resolve(__dirname, '../../tests');

let resolvedPjPath;
try {
resolvedPjPath = require.resolve(`${this.packageName}/package.json`, {
paths: [searchPath],
});
} catch (e) {
throw new Error(`${this.packageName} not found preinstalled (searching from ${searchPath}): ${e}`);
}
const pj = JSON.parse(await readFile(resolvedPjPath, 'utf-8'));
const version = pj.version;

// Walk up from the resolved package.json to the `node_modules` directory it lives directly under,
// so we can add `<node_modules>/.bin` to the $PATH (same bin layout as a regular npm install).
const packageDir = path.dirname(resolvedPjPath);
const nodeModulesDir = path.resolve(packageDir, ...this.packageName.startsWith('@') ? ['..', '..'] : ['..']);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Recommended — RunnerCliPreinstalledSource.runnerPrepare() re-implements the resolve-and-read-version block that already exists as the static preinstalledVersion(packageName) on the library preinstalled source: the same searchPath = path.resolve(__dirname, '../../tests'), the same require.resolve("<pkg>/package.json", { paths: [searchPath] }), the same try/catch, and the same JSON.parse(readFile(...)).version. Two copies of this resolution logic will drift — a future change to the search path or error wording lands in one and not the other.

Suggested change: Extract a shared helper (e.g. resolvePreinstalledPackageJson(packageName): string) that returns the resolved package.json path, and have both the library source's preinstalledVersion and this new CLI source call it — the CLI source reads the version and derives binDir from the returned path.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dgandhi62 is this accurate?

Comment on lines +11 to +23
const testsNodeModules = path.resolve(__dirname, '..', 'tests', 'node_modules');
const fakePackageDir = path.join(testsNodeModules, FAKE_PACKAGE_NAME);

afterEach(async () => {
await fs.rm(fakePackageDir, { recursive: true, force: true });
});

test('resolves version and .bin dir from a preinstalled package under tests/node_modules', async () => {
await fs.mkdir(fakePackageDir, { recursive: true });
await fs.writeFile(
path.join(fakePackageDir, 'package.json'),
JSON.stringify({ name: FAKE_PACKAGE_NAME, version: '9.9.9' }),
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚪ Optional — The test creates fakePackageDir under the real, committed tests/node_modules directory with fs.mkdir(..., { recursive: true }). When tests/node_modules does not already exist in a checkout, recursive: true creates it, but afterEach only removes fakePackageDir, so the run can leave a stray empty tests/node_modules behind in a real source directory. The fixture name is collision-proof and cleanup uses { recursive: true, force: true }, so this is not a correctness or parallel-run concern — only unwanted residue.

Suggested change: Point cleanup at the directory the test actually created (remove tests/node_modules only if the test created it), or assert that tests/node_modules already exists before writing the fixture.

…ization

The test process reconstructs each package source from $SOURCES by looking
up its class name in the CONSTRUCTORS list in subprocess.ts. The preinstalled
sources (TestCliPreinstalledSource, TestLibraryPreinstalledSource) and
TestLibraryGlobalInstallSource were never registered there, so any test using
a withCdkApp-family fixture failed at TestFixture construction with
'Unrecognized constructor' once a --*-preinstalled flag was used.

Register the three missing test-side source classes.
}
}

export class TestCliPreinstalledSource implements ITestCliSource {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can reuse TestCliNpmSource:

export class TestCliNpmSource implements ITestCliSource {
constructor(private readonly installRoot: string, private readonly range: string) {
}
public async makeCliAvailable() {
addToShellPath(path.join(this.installRoot, 'node_modules', '.bin'));
}
public requestedVersion() {
return this.range;
}
}

Just maybe rename it to TestCliSource since it really is not tied to NPM anyway.

}

// Preinstalled cdk-assets: already available in node_modules/.bin or on $PATH, no network install.
if (args['cdk-assets-preinstalled']) {

@iliapolo iliapolo Oct 21, 2026 •

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see the intent but I think it creates an options inflation because we also want the user to be able to pass in a custom path. So you'd need cdk-assets-preinstalled-path in addition to this.

Lets just have cdk-assets-path instead (similarly to cdk-assets-version which we already have). So if a user wants to use a pre-installed package, they must tell us where is it installed.

*
* Resolves the package with `require.resolve`, so this performs no npm network calls.
*/
export class RunnerCliPreinstalledSource implements IRunnerSource<ITestCliSource> {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
export class RunnerCliPreinstalledSource implements IRunnerSource<ITestCliSource> {
export class RunnerCliPathSource implements IRunnerSource<ITestCliSource> {

Comment on lines +18 to +37

public async runnerPrepare(): Promise<IPreparedRunnerSource<ITestCliSource>> {
// Pretend to be in the test directory and resolve the package
const searchPath = path.resolve(__dirname, '../../tests');

let resolvedPjPath;
try {
resolvedPjPath = require.resolve(`${this.packageName}/package.json`, {
paths: [searchPath],
});
} catch (e) {
throw new Error(`${this.packageName} not found preinstalled (searching from ${searchPath}): ${e}`);
}
const pj = JSON.parse(await readFile(resolvedPjPath, 'utf-8'));
const version = pj.version;

// Walk up from the resolved package.json to the `node_modules` directory it lives directly under,
// so we can add `<node_modules>/.bin` to the $PATH (same bin layout as a regular npm install).
const packageDir = path.dirname(resolvedPjPath);
const nodeModulesDir = path.resolve(packageDir, ...this.packageName.startsWith('@') ? ['..', '..'] : ['..']);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dgandhi62 is this accurate?

This branch had an error being deployed

1 failed (outdated) and 2 active deployments
run-tests — d0217f5d Deployed Oct 21, 2026 by dgandhi62 via integ_cli (cli-integ-tests, 24.19, 3) #7382
no-approval — d0217f5d Deployed Oct 21, 2026 by dgandhi62 via prepare #7382
automation — df5cf404 Deployed Oct 20, 2026 by dgandhi62 via Triage Pull Requests #2223
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants