diff --git a/packages/aws-cdk/lib/util/npm.ts b/packages/aws-cdk/lib/util/npm.ts new file mode 100644 index 0000000000000..c46167342a5a8 --- /dev/null +++ b/packages/aws-cdk/lib/util/npm.ts @@ -0,0 +1,19 @@ +import { exec as _exec } from 'child_process'; +import { promisify } from 'util'; +import * as semver from 'semver'; +import { debug } from '../../lib/logging'; + +const exec = promisify(_exec); + +export async function getLatestVersionFromNpm(): Promise { + const { stdout, stderr } = await exec('npm view aws-cdk version'); + 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 Error(`npm returned an invalid semver ${latestVersion}`); + } + + return latestVersion; +} diff --git a/packages/aws-cdk/lib/version.ts b/packages/aws-cdk/lib/version.ts index e407935ce2bfa..01cfbe0dae463 100644 --- a/packages/aws-cdk/lib/version.ts +++ b/packages/aws-cdk/lib/version.ts @@ -1,16 +1,17 @@ -import { exec as _exec } from 'child_process'; import * as path from 'path'; -import { promisify } from 'util'; import * as colors from 'colors/safe'; import * as fs from 'fs-extra'; import * as semver from 'semver'; import { debug, print } from '../lib/logging'; import { formatAsBanner } from '../lib/util/console-formatters'; import { cdkCacheDir } from './util/directories'; +import { getLatestVersionFromNpm } from './util/npm'; const ONE_DAY_IN_SECONDS = 1 * 24 * 60 * 60; -const exec = promisify(_exec); +const UPGRADE_DOCUMENTATION_LINKS: Record = { + 1: 'https://docs.aws.amazon.com/cdk/v2/guide/migrating-v2.html', +}; export const DISPLAY_VERSION = `${versionNumber()} (build ${commit()})`; @@ -79,14 +80,7 @@ export async function latestVersionIfHigher(currentVersion: string, cacheFile: V return null; } - const { stdout, stderr } = await exec('npm view aws-cdk version'); - 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 Error(`npm returned an invalid semver ${latestVersion}`); - } + const latestVersion = await getLatestVersionFromNpm(); const isNewer = semver.gt(latestVersion, currentVersion); await cacheFile.update(latestVersion); @@ -97,19 +91,30 @@ export async function latestVersionIfHigher(currentVersion: string, cacheFile: V } } -export async function displayVersionMessage(): Promise { +function getMajorVersionUpgradeMessage(currentVersion: string): string | void { + const currentMajorVersion = semver.major(currentVersion); + if (UPGRADE_DOCUMENTATION_LINKS[currentMajorVersion]) { + return `Information about upgrading from version ${currentMajorVersion}.x to version ${currentMajorVersion + 1}.x is available here: ${UPGRADE_DOCUMENTATION_LINKS[currentMajorVersion]}`; + } +} + +function getVersionMessage(currentVersion: string, laterVersion: string): string[] { + return [ + `Newer version of CDK is available [${colors.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 { if (!process.stdout.isTTY || process.env.CDK_DISABLE_VERSION_CHECK) { return; } try { - const versionCheckCache = new VersionCheckTTL(); - const laterVersion = await latestVersionIfHigher(versionNumber(), versionCheckCache); + const laterVersion = await latestVersionIfHigher(currentVersion, versionCheckCache ?? new VersionCheckTTL()); if (laterVersion) { - const bannerMsg = formatAsBanner([ - `Newer version of CDK is available [${colors.green(laterVersion as string)}]`, - 'Upgrade recommended (npm install -g aws-cdk)', - ]); + const bannerMsg = formatAsBanner(getVersionMessage(currentVersion, laterVersion)); bannerMsg.forEach((e) => print(e)); } } catch (err) { diff --git a/packages/aws-cdk/test/version.test.ts b/packages/aws-cdk/test/version.test.ts index 01b120c463a6b..9019de161d159 100644 --- a/packages/aws-cdk/test/version.test.ts +++ b/packages/aws-cdk/test/version.test.ts @@ -4,6 +4,7 @@ import { promisify } from 'util'; import * as fs from 'fs-extra'; import * as sinon from 'sinon'; import * as logging from '../lib/logging'; +import * as npm from '../lib/util/npm'; import { latestVersionIfHigher, VersionCheckTTL, displayVersionMessage } from '../lib/version'; jest.setTimeout(10_000); @@ -77,9 +78,61 @@ test('No Version specified for storage in the TTL file', async () => { }); test('Skip version check if environment variable is set', async () => { - process.stdout.isTTY = true; - process.env.CDK_DISABLE_VERSION_CHECK = '1'; + sinon.stub(process, 'stdout').value({ ...process.stdout, isTTY: true }); + sinon.stub(process, 'env').value({ ...process.env, CDK_DISABLE_VERSION_CHECK: '1' }); const printStub = sinon.stub(logging, 'print'); await displayVersionMessage(); expect(printStub.called).toEqual(false); }); + +describe('version message', () => { + let previousIsTty: true | undefined; + beforeAll(() => { + previousIsTty = process.stdout.isTTY; + process.stdout.isTTY = true; + }); + + afterAll(() => { + 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').mockResolvedValue('1.1.0'); + const printSpy = jest.spyOn(logging, 'print'); + + // 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').mockResolvedValue('2.0.0'); + const printSpy = jest.spyOn(logging, 'print'); + + // 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').mockResolvedValue('100.0.0'); + const printSpy = jest.spyOn(logging, 'print'); + + // 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')); + }); +});