Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(toolkit): show when new version is available #2484

Merged
merged 4 commits into from
May 10, 2019
Merged
Show file tree
Hide file tree
Changes from 2 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
5 changes: 3 additions & 2 deletions packages/aws-cdk/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,12 @@ node_modules
dist

# Generated by generate.sh
lib/version.ts
build-info.json
.LAST_VERSION_CHECK

.LAST_BUILD
.nyc_output
coverage
.nycrc
.LAST_PACKAGE
*.snk
*.snk
29 changes: 16 additions & 13 deletions packages/aws-cdk/bin/cdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import { PluginHost } from '../lib/plugin';
import { parseRenames } from '../lib/renames';
import { serializeStructure } from '../lib/serialize';
import { Configuration, Settings } from '../lib/settings';
import { VERSION } from '../lib/version';
import version = require('../lib/version');

// tslint:disable-next-line:no-var-requires
const promptly = require('promptly');
Expand Down Expand Up @@ -76,7 +76,7 @@ async function parseCommandLineArguments() {
.option('language', { type: 'string', alias: 'l', desc: 'the language to be used for the new project (default can be configured in ~/.cdk.json)', choices: initTemplateLanuages })
.option('list', { type: 'boolean', desc: 'list the available templates' }))
.commandDir('../lib/commands', { exclude: /^_.*/ })
.version(VERSION)
.version(version.DISPLAY_VERSION)
.demandCommand(1, '') // just print help
.help()
.alias('h', 'help')
Expand All @@ -96,8 +96,7 @@ async function initCommandLine() {
if (argv.verbose) {
setVerbose();
}

debug('CDK toolkit version:', VERSION);
debug('CDK toolkit version:', version.DISPLAY_VERSION);
debug('Command line arguments:', argv);

const aws = new SDK({
Expand Down Expand Up @@ -152,15 +151,19 @@ async function initCommandLine() {
// Bundle up global objects so the commands have access to them
const commandOptions = { args: argv, appStacks, configuration, aws };

const returnValue = argv.commandHandler
? await (argv.commandHandler as (opts: typeof commandOptions) => any)(commandOptions)
: await main(cmd, argv);
if (typeof returnValue === 'object') {
return toJsonOrYaml(returnValue);
} else if (typeof returnValue === 'string') {
return returnValue;
} else {
return returnValue;
try {
const returnValue = argv.commandHandler
? await (argv.commandHandler as (opts: typeof commandOptions) => any)(commandOptions)
: await main(cmd, argv);
if (typeof returnValue === 'object') {
return toJsonOrYaml(returnValue);
} else if (typeof returnValue === 'string') {
return returnValue;
} else {
return returnValue;
}
} finally {
await version.displayVersionMessage();
}

async function main(command: string, args: any): Promise<number | string | {} | void> {
Expand Down
13 changes: 6 additions & 7 deletions packages/aws-cdk/generate.sh
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,9 @@ if [ -z "${commit}" ]; then
commit="$(git rev-parse --verify HEAD)"
fi

cat > lib/version.ts <<HERE
// Generated at $(date -u +"%Y-%m-%dT%H:%M:%SZ") by generate.sh

/** The qualified version number for this CDK toolkit. */
// tslint:disable-next-line:no-var-requires
export const VERSION = \`\${require('../package.json').version.replace(/\\+[0-9a-f]+\$/, '')} (build ${commit:0:7})\`;
HERE
cat > build-info.json <<HERE
{
"comment": "Generated at $(date -u +"%Y-%m-%dT%H:%M:%SZ") by generate.sh",
"commit": "${commit:0:7}"
}
HERE
2 changes: 2 additions & 0 deletions packages/aws-cdk/lib/commands/context.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import colors = require('colors/safe');
import yargs = require('yargs');
import version = require('../../lib/version');
import { CommandOptions } from '../command-api';
import { print } from '../logging';
import { Context, PROJECT_CONFIG } from '../settings';
Expand Down Expand Up @@ -44,6 +45,7 @@ export async function realHandler(options: CommandOptions): Promise<number> {
listContext(contextValues);
}
}
await version.displayVersionMessage();

return 0;
}
Expand Down
5 changes: 3 additions & 2 deletions packages/aws-cdk/lib/commands/doctor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import colors = require('colors/safe');
import process = require('process');
import yargs = require('yargs');
import { print } from '../../lib/logging';
import { VERSION } from '../../lib/version';
import version = require('../../lib/version');
import { CommandOptions } from '../command-api';

export const command = 'doctor';
Expand All @@ -21,6 +21,7 @@ export async function realHandler(_options: CommandOptions): Promise<number> {
exitStatus = -1;
}
}
await version.displayVersionMessage();
return exitStatus;
}

Expand All @@ -33,7 +34,7 @@ const verifications: Array<() => boolean | Promise<boolean>> = [
// ### Verifications ###

function displayVersionInformation() {
print(`ℹ️ CDK Version: ${colors.green(VERSION)}`);
print(`ℹ️ CDK Version: ${colors.green(version.DISPLAY_VERSION)}`);
return true;
}

Expand Down
98 changes: 98 additions & 0 deletions packages/aws-cdk/lib/version.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { exec as _exec } from 'child_process';
import colors = require('colors/safe');
import { open as _open, stat as _stat } from 'fs';
import semver = require('semver');
import { promisify } from 'util';
import { debug, print, warning } from '../lib/logging';

const ONE_DAY_IN_SECONDS = 1 * 24 * 60 * 60;

const exec = promisify(_exec);
const open = promisify(_open);
const stat = promisify(_stat);

export const DISPLAY_VERSION = `${versionNumber()} (build ${commit()})`;

function versionNumber(): string {
return require('../package.json').version.replace(/\+[0-9a-f]+$/, '');
}

function commit(): string {
return require('../build-info.json').commit;
}

export class CacheFile {
nija-at marked this conversation as resolved.
Show resolved Hide resolved
private readonly file: string;

// File modify times are accurate only till the second, hence using seconds as precision
private readonly ttlSecs: number;

constructor(file: string, ttlSecs: number) {
this.file = file;
this.ttlSecs = ttlSecs;
}

public async hasExpired(): Promise<boolean> {
try {
const lastCheckTime = (await stat(this.file)).mtimeMs;
const today = new Date().getTime();

if ((today - lastCheckTime) / 1000 > this.ttlSecs) { // convert ms to secs
return true;
}
return false;
} catch (err) {
if (err.code === 'ENOENT') {
return true;
} else {
throw err;
}
}
}

public async update(): Promise<void> {
await open(this.file, 'w');
nija-at marked this conversation as resolved.
Show resolved Hide resolved
}
}

// Export for unit testing only.
// Don't use directly, use displayVersionMessage() instead.
export async function latestVersionIfHigher(currentVersion: string, cacheFile: CacheFile): Promise<string | null> {
Copy link
Contributor

Choose a reason for hiding this comment

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

Don't export it if you don't want it to be used.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Any suggestions on how else I can (unit) test the logic here? Or would you rather not export and have no test?

if (!(await cacheFile.hasExpired())) {
return null;
}

const { stdout, stderr } = await exec(`npm view cdk version`);
nija-at marked this conversation as resolved.
Show resolved Hide resolved
if (stderr && stderr.trim().length > 0) {
debug(`The 'npm view' command generated an error stream with content [${stderr.trim()}]`);
nija-at marked this conversation as resolved.
Show resolved Hide resolved
}
const latestVersion = stdout.trim();
if (!semver.valid(latestVersion)) {
throw new Error(`npm returned an invalid semver ${latestVersion}`);
}
const isNewer = semver.gt(latestVersion, currentVersion);
await cacheFile.update();

if (isNewer) {
return latestVersion;
} else {
return null;
}
}

const versionCheckCache = new CacheFile(`${__dirname}/../.LAST_VERSION_CHECK`, ONE_DAY_IN_SECONDS);

export async function displayVersionMessage(): Promise<void> {
try {
const laterVersion = await latestVersionIfHigher(versionNumber(), versionCheckCache);
if (laterVersion) {
const fmt = colors.green(laterVersion as string);
print('********************************************************');
print(`*** Newer version of the CDK is available [${fmt}] ***`);
nija-at marked this conversation as resolved.
Show resolved Hide resolved
print(`*** Upgrade now by running "npm up -g cdk" ***`);
nija-at marked this conversation as resolved.
Show resolved Hide resolved
print('********************************************************');
}
} catch (err) {
warning(`Could not run version check due to error ${err.message} - ${err.stack}`);
nija-at marked this conversation as resolved.
Show resolved Hide resolved
}
}
52 changes: 52 additions & 0 deletions packages/aws-cdk/test/test.version.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { Test } from 'nodeunit';
import { setTimeout as _setTimeout } from 'timers';
import { promisify } from 'util';
import { CacheFile, latestVersionIfHigher } from '../lib/version';

const setTimeout = promisify(_setTimeout);

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

export = {
async 'cache file responds correctly when file is not present'(test: Test) {
const cache = new CacheFile(tmpfile(), 1);
test.strictEqual(await cache.hasExpired(), true);
test.done();
},

async 'cache file honours the specified TTL'(test: Test) {
const cache = new CacheFile(tmpfile(), 1);
await cache.update();
test.strictEqual(await cache.hasExpired(), false);
await setTimeout(1000); // 1 sec in ms
test.strictEqual(await cache.hasExpired(), true);
test.done();
},

async 'Skip version check if cache has not expired'(test: Test) {
const cache = new CacheFile(tmpfile(), 100);
await cache.update();
test.equal(await latestVersionIfHigher('0.0.0', cache), null);
test.done();
},

async 'Return later version when exists & skip recent re-check'(test: Test) {
const cache = new CacheFile(tmpfile(), 100);
const result = await latestVersionIfHigher('0.0.0', cache);
test.notEqual(result, null);
test.ok((result as string).length > 0);

const result2 = await latestVersionIfHigher('0.0.0', cache);
test.equal(result2, null);
test.done();
},

async 'Return null if version is higher than npm'(test: Test) {
const cache = new CacheFile(tmpfile(), 100);
const result = await latestVersionIfHigher('100.100.100', cache);
test.equal(result, null);
test.done();
},
};