Skip to content
Merged
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
38 changes: 25 additions & 13 deletions packages/aws-cdk/lib/cli/util/npm.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,34 @@
import { exec as _exec } from 'child_process';
import { promisify } from 'util';
import * as semver from 'semver';
import { ToolkitError } from '../../../../@aws-cdk/tmp-toolkit-helpers/src/api';
import { debug } from '../../logging';

const exec = promisify(_exec);

/* c8 ignore start */ // not called during unit tests
export async function getLatestVersionFromNpm(): Promise<string> {
const { stdout, stderr } = await exec('npm view aws-cdk version', { timeout: 3000 });
if (stderr && stderr.trim().length > 0) {
debug(`The 'npm view' command generated an error stream with content [${stderr.trim()}]`);
}
const latestVersion = stdout.trim();
if (!semver.valid(latestVersion)) {
throw new ToolkitError(`npm returned an invalid semver ${latestVersion}`);
}
/* c8 ignore start */
export async function execNpmView(currentVersion: string) {
try {
// eslint-disable-next-line @cdklabs/promiseall-no-unbounded-parallelism
const [latestResult, currentResult] = await Promise.all([
exec('npm view aws-cdk@latest version', { timeout: 3000 }),
exec(`npm view aws-cdk@${currentVersion} name version deprecated --json`, { timeout: 3000 }),
]);

if (latestResult.stderr && latestResult.stderr.trim().length > 0) {
throw new ToolkitError(`npm view command for latest version failed: ${latestResult.stderr.trim()}`);
}
if (currentResult.stderr && currentResult.stderr.trim().length > 0) {
throw new ToolkitError(`npm view command for current version failed: ${currentResult.stderr.trim()}`);
}

return latestVersion;
const latestVersion = latestResult.stdout;
const currentInfo = JSON.parse(currentResult.stdout);

return {
latestVersion: latestVersion,
deprecated: currentInfo.deprecated,
};
} catch (err: unknown) {
throw err;
}
}
/* c8 ignore stop */
43 changes: 20 additions & 23 deletions packages/aws-cdk/lib/cli/version.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { debug, info } from '../logging';
import { cdkCacheDir } from '../util';
import { cliRootDir } from './root-dir';
import { formatAsBanner } from './util/console-formatters';
import { getLatestVersionFromNpm } from './util/npm';
import { execNpmView } from './util/npm';

const ONE_DAY_IN_SECONDS = 1 * 24 * 60 * 60;

Expand Down Expand Up @@ -84,20 +84,28 @@ export class VersionCheckTTL {

// Export for unit testing only.
// Don't use directly, use displayVersionMessage() instead.
export async function latestVersionIfHigher(currentVersion: string, cacheFile: VersionCheckTTL): Promise<string|null> {
export async function getVersionMessages(currentVersion: string, cacheFile: VersionCheckTTL): Promise<string[]> {
if (!(await cacheFile.hasExpired())) {
return null;
return [];
}

const latestVersion = await getLatestVersionFromNpm();
const isNewer = semver.gt(latestVersion, currentVersion);
await cacheFile.update(latestVersion);
const packageInfo = await execNpmView(currentVersion);
const latestVersion = packageInfo.latestVersion;
await cacheFile.update(JSON.stringify(packageInfo));

if (isNewer) {
return latestVersion;
} else {
return null;
// If the latest version is the same as the current version, there is no need to display a message
if (semver.eq(latestVersion, currentVersion)) {
return [];
}
Comment on lines +97 to 99
Copy link
Contributor

Choose a reason for hiding this comment

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

This is only correct if you assume the latest pointer never points to a deprecated version. If that's something you assume, you should put it in a comment.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I added the comment 👍


const versionMessage = [
packageInfo.deprecated ? `${chalk.red(packageInfo.deprecated as string)}` : undefined,
`Newer version of CDK is available [${chalk.green(latestVersion as string)}]`,
getMajorVersionUpgradeMessage(currentVersion),
'Upgrade recommended (npm install -g aws-cdk)',
].filter(Boolean) as string[];

return versionMessage;
}

function getMajorVersionUpgradeMessage(currentVersion: string): string | void {
Expand All @@ -107,25 +115,14 @@ function getMajorVersionUpgradeMessage(currentVersion: string): string | void {
}
}

function getVersionMessage(currentVersion: string, laterVersion: string): string[] {
return [
`Newer version of CDK is available [${chalk.green(laterVersion as string)}]`,
getMajorVersionUpgradeMessage(currentVersion),
'Upgrade recommended (npm install -g aws-cdk)',
].filter(Boolean) as string[];
}

export async function displayVersionMessage(currentVersion = versionNumber(), versionCheckCache?: VersionCheckTTL): Promise<void> {
if (!process.stdout.isTTY || process.env.CDK_DISABLE_VERSION_CHECK) {
return;
}

try {
const laterVersion = await latestVersionIfHigher(currentVersion, versionCheckCache ?? new VersionCheckTTL());
if (laterVersion) {
const bannerMsg = formatAsBanner(getVersionMessage(currentVersion, laterVersion));
bannerMsg.forEach((e) => info(e));
}
const versionMessages = await getVersionMessages(currentVersion, versionCheckCache ?? new VersionCheckTTL());
formatAsBanner(versionMessages).forEach(e => info(e));
} catch (err: any) {
debug(`Could not run version check - ${err.message}`);
}
Expand Down
2 changes: 1 addition & 1 deletion packages/aws-cdk/lib/legacy-exports-source.ts
Copy link
Contributor

Choose a reason for hiding this comment

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

please make sure no other changes than the removal are in this file

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I removed unnecessary changes.

Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export { cli, exec } from './cli/cli';
export { cliRootDir as rootDir } from './cli/root-dir';
export { Command, Configuration, PROJECT_CONTEXT } from './cli/user-configuration';
export { formatAsBanner } from './cli/util/console-formatters';
export { latestVersionIfHigher, versionNumber } from './cli/version';
export { versionNumber } from './cli/version';

// Commands
export { RequireApproval } from './commands/diff';
Expand Down
1 change: 0 additions & 1 deletion packages/aws-cdk/lib/legacy-exports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,6 @@ export const {
deepMerge,
Deployments,
rootDir,
latestVersionIfHigher,
versionNumber,
availableInitTemplates,
cached,
Expand Down
127 changes: 63 additions & 64 deletions packages/aws-cdk/test/cli/version.test.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,24 @@
/* eslint-disable import/order */
import * as path from 'path';
import { setTimeout as _setTimeout } from 'timers';
import { promisify } from 'util';
import * as fs from 'fs-extra';
import * as os from 'os';
import * as path from 'path';
import * as sinon from 'sinon';
import * as logging from '../../lib/logging';
import { setTimeout as _setTimeout } from 'timers';
import { promisify } from 'util';
import * as npm from '../../lib/cli/util/npm';
import { latestVersionIfHigher, VersionCheckTTL, displayVersionMessage, isDeveloperBuild } from '../../lib/cli/version';
import { displayVersionMessage, getVersionMessages, isDeveloperBuild, VersionCheckTTL } from '../../lib/cli/version';
import * as logging from '../../lib/logging';

jest.setTimeout(10_000);


const setTimeout = promisify(_setTimeout);

function tmpfile(): string {
return `/tmp/version-${Math.floor(Math.random() * 10000)}`;
}

beforeEach(() => {
jest.spyOn(npm, 'getLatestVersionFromNpm').mockRejectedValue(new Error('expected version cache to be used, but call to npm was attempted'));
process.chdir(os.tmpdir()); // Need a chdir because in the workspace 'npm view' will take a long time
});

Expand Down Expand Up @@ -49,25 +49,7 @@ test('cache file honours the specified TTL', async () => {
test('Skip version check if cache has not expired', async () => {
const cache = new VersionCheckTTL(tmpfile(), 100);
await cache.update();
expect(await latestVersionIfHigher('0.0.0', cache)).toBeNull();
});

test('Return later version when exists & skip recent re-check', async () => {
const cache = new VersionCheckTTL(tmpfile(), 100);
jest.spyOn(npm, 'getLatestVersionFromNpm').mockResolvedValueOnce('1.1.0');
const result = await latestVersionIfHigher('0.0.0', cache);
expect(result).not.toBeNull();
expect((result as string).length).toBeGreaterThan(0);

const result2 = await latestVersionIfHigher('0.0.0', cache);
expect(result2).toBeNull();
});

test('Return null if version is higher than npm', async () => {
const cache = new VersionCheckTTL(tmpfile(), 100);
jest.spyOn(npm, 'getLatestVersionFromNpm').mockResolvedValueOnce('1.1.0');
const result = await latestVersionIfHigher('100.100.100', cache);
expect(result).toBeNull();
expect(await getVersionMessages('0.0.0', cache)).toEqual([]);
});

test('Version specified is stored in the TTL file', async () => {
Expand Down Expand Up @@ -104,45 +86,62 @@ describe('version message', () => {
process.stdout.isTTY = previousIsTty;
});

test('Prints a message when a new version is available', async () => {
// Given the current version is 1.0.0 and the latest version is 1.1.0
const currentVersion = '1.0.0';
jest.spyOn(npm, 'getLatestVersionFromNpm').mockResolvedValueOnce('1.1.0');
const printSpy = jest.spyOn(logging, 'info');

// When displayVersionMessage is called
await displayVersionMessage(currentVersion, new VersionCheckTTL(tmpfile(), 0));

// Then the new version message is printed to stdout
expect(printSpy).toHaveBeenCalledWith(expect.stringContaining('1.1.0'));
});

test('Includes major upgrade documentation when available', async() => {
// Given the current version is 1.0.0 and the latest version is 2.0.0
const currentVersion = '1.0.0';
jest.spyOn(npm, 'getLatestVersionFromNpm').mockResolvedValueOnce('2.0.0');
const printSpy = jest.spyOn(logging, 'info');

// When displayVersionMessage is called
await displayVersionMessage(currentVersion, new VersionCheckTTL(tmpfile(), 0));

// Then the V1 -> V2 documentation is printed
expect(printSpy).toHaveBeenCalledWith(expect.stringContaining('Information about upgrading from version 1.x to version 2.x is available here: https://docs.aws.amazon.com/cdk/v2/guide/migrating-v2.html'));
});

test('Does not include major upgrade documentation when unavailable', async() => {
// Given current version is 99.0.0 and the latest version is 100.0.0
const currentVersion = '99.0.0';
jest.spyOn(npm, 'getLatestVersionFromNpm').mockResolvedValueOnce('100.0.0');
const printSpy = jest.spyOn(logging, 'info');

// When displayVersionMessage is called
await displayVersionMessage(currentVersion, new VersionCheckTTL(tmpfile(), 0));

// Then no upgrade documentation is printed
expect(printSpy).toHaveBeenCalledWith(expect.stringContaining('100.0.0'));
expect(printSpy).not.toHaveBeenCalledWith(expect.stringContaining('Information about upgrading from 99.x to 100.x'));
});
test('Prints messages when a new version is available', async () => {
const mockCache = new VersionCheckTTL(tmpfile());
jest.spyOn(mockCache, 'hasExpired').mockResolvedValue(true);

jest.spyOn(npm, 'execNpmView').mockResolvedValue({
latestVersion: '2.0.0',
deprecated: undefined,
});

const messages = await getVersionMessages('1.0.0', mockCache);
expect(messages.some(msg => msg.includes('Newer version of CDK is available'))).toBeTruthy();
expect(messages.some(msg => msg.includes('Information about upgrading from version 1.x to version 2.x'))).toBeTruthy();
expect(messages.some(msg => msg.includes('Upgrade recommended (npm install -g aws-cdk)'))).toBeTruthy();
})

test('Does not include major upgrade documentation when unavailable', async () => {
const mockCache = new VersionCheckTTL(tmpfile());
jest.spyOn(mockCache, 'hasExpired').mockResolvedValue(true);

jest.spyOn(npm, 'execNpmView').mockResolvedValue({
latestVersion: '2.1000.0',
deprecated: undefined,
});

const messages = await getVersionMessages('2.179.0', mockCache);
const hasUpgradeDoc = messages.some(msg =>
msg.includes('Information about upgrading from version 1.x to version 2.x')
);
expect(hasUpgradeDoc).toBeFalsy();
})

test('Prints a message when a deprecated version is used', async () => {
const mockCache = new VersionCheckTTL(tmpfile());
jest.spyOn(mockCache, 'hasExpired').mockResolvedValue(true);

jest.spyOn(npm, 'execNpmView').mockResolvedValue({
latestVersion: '2.0.0',
deprecated: 'This version is deprecated.',
});

const messages = await getVersionMessages('1.0.0', mockCache);
expect(messages.some(msg => msg.includes('This version is deprecated'))).toBeTruthy();
})

test('Does not print message when current version is up to date', async () => {
const mockCache = new VersionCheckTTL(tmpfile());
jest.spyOn(mockCache, 'hasExpired').mockResolvedValue(true);

jest.spyOn(npm, 'execNpmView').mockResolvedValue({
latestVersion: '1.0.0',
deprecated: undefined,
});

const messages = await getVersionMessages('1.0.0', mockCache);
expect(messages).toEqual([]);
})
});

test('isDeveloperBuild call does not throw an error', () => {
Expand Down