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

ncu-ci: command that shows results of jenkins #161

Merged
merged 5 commits into from
Jun 1, 2018
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ tmp
coverage.lcov
tmp-*
.eslintcache
.ncu
166 changes: 166 additions & 0 deletions bin/ncu-ci
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
#!/usr/bin/env node

'use strict';

const {
PRBuild, BenchmarkRun, CommitBuild, parseJobFromURL,
constants, JobParser
// , jobCache
} = require('../lib/ci');
const clipboardy = require('clipboardy');

const {
PR, COMMIT, BENCHMARK
} = constants;

const { runPromise } = require('../lib/run');
const auth = require('../lib/auth');
const Request = require('../lib/request');
const CLI = require('../lib/cli');
const yargs = require('yargs');

// This is used for testing
// Default cache dir is ${ncu-source-dir}/.ncu/cache
// jobCache.enable();

// eslint-disable-next-line no-unused-vars
const argv = yargs
.command({
command: 'url <url>',
desc: 'Automatically detect CI type and show results',
builder: (yargs) => {
yargs
.positional('url', {
describe: 'URL of the PR or the CI',
type: 'string'
});
},
handler
})
.command({
command: 'pr <jobid>',
desc: 'Show results of a node-test-pull-request CI job',
builder: (yargs) => {
yargs
.positional('jobid', {
describe: 'id of the job',
type: 'number'
});
},
handler
})
.command({
command: 'commit <jobid>',
desc: 'Show results of a node-test-commit CI job',
builder: (yargs) => {
yargs
.positional('jobid', {
describe: 'id of the job',
type: 'number'
});
},
handler
})
.command({
command: 'benchmark <jobid>',
desc: 'Show results of a benchmark-node-micro-benchmarks CI job',
builder: (yargs) => {
yargs
.positional('jobid', {
describe: 'id of the job',
type: 'number'
});
},
handler
})
.demandCommand(1, 'must provide a valid command')
.option('copy', {
default: false,
describe: 'Write the results as markdown to clipboard'
})
.help()
.argv;

async function getResults(cli, request, job) {
let build;
const { type, jobid } = job;
if (type === PR) {
build = new PRBuild(cli, request, jobid);
await build.getResults();
} else if (type === COMMIT) {
build = new CommitBuild(cli, request, jobid);
await build.getResults();
} else if (type === BENCHMARK) {
build = new BenchmarkRun(cli, request, jobid);
await build.getResults();
} else {
yargs.showHelp();
return;
}
return build;
}

async function main(command, argv) {
const cli = new CLI();
const credentials = await auth();
const request = new Request(credentials);
const queue = [];

const commandToType = {
'commit': COMMIT,
'pr': PR,
'benchmark': BENCHMARK
};

if (command === 'url') {
let parsed = parseJobFromURL(argv.url);
if (parsed) {
queue.push({
type: parsed.type,
jobid: parsed.jobid,
copy: argv.copy
});
} else {
const parser = await JobParser.fromPR(argv.url, cli, request);
if (!parser) { // Not a valid PR URL
return yargs.showHelp();
}
const ciMap = parser.parse();
for (const [type, ci] of ciMap) {
queue.push({
type: type,
jobid: ci.jobid,
copy: argv.copy
});
}
}
} else {
queue.push({
type: commandToType[command],
jobid: argv.jobid,
copy: argv.copy
});
}

let dataToCopy = '';

for (let job of queue) {
const build = await getResults(cli, request, job);
build.display();

if (argv.copy) {
dataToCopy += build.formatAsMarkdown();
}
}

if (argv.copy) {
clipboardy.writeSync(dataToCopy);
cli.separator('');
cli.log(`Written markdown to clipboard`);
}
}

function handler(argv) {
const [ command ] = argv._;
runPromise(main(command, argv));
}
47 changes: 47 additions & 0 deletions docs/ncu-ci.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# ncu-ci

Parse the results of a Jenkins CI run in https://ci.nodejs.org/ and display
a summary for all the failures.

Supported jobs:

- [node-test-pull-request](https://ci.nodejs.org/job/node-test-pull-request)
- [node-test-commit](https://ci.nodejs.org/job/node-test-commit)
- [benchmark-node-micro-benchmarks](https://ci.nodejs.org/job/benchmark-node-micro-benchmarks/)

```
ncu-ci <command>

Commands:
ncu-ci url <url> Automatically detect CI type and show results
ncu-ci pr <jobid> Show results of a node-test-pull-request CI job
ncu-ci commit <jobid> Show results of a node-test-commit CI job
ncu-ci benchmark <jobid> Show results of a benchmark-node-micro-benchmarks CI
job

Options:
--version Show version number [boolean]
--copy Write the results as markdown to clipboard [default: false]
--help Show help [boolean]
```

## Example

Get the CI results of PR 12345 (including latest results of each type of
supported CI) and copy the summaries into clipboard:

```
ncu-ci url https://github.com/nodejs/node/pull/12345 --copy
```

Get the results of job #12345 of `node-test-pull-request`:

```
ncu-ci pr 12345
```

## Caveats

The CI failures are parsed using pattern matching and could be incorrect. Feel
free to open a pull request whenever you find a case that ncu-ci does not handle
well.
107 changes: 107 additions & 0 deletions lib/cache.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
'use strict';

const path = require('path');
const fs = require('fs');
const { writeJson, readJson, writeFile, readFile } = require('./file');

function isAsync(fn) {
return fn[Symbol.toStringTag] === 'AsyncFunction';
}

class Cache {
constructor(dir) {
this.dir = dir || this.computeCacheDir(path.join(__dirname, '..'));
this.originals = {};
this.disabled = true;
}

computeCacheDir(base) {
return path.join(base, '.ncu', 'cache');
}

disable() {
this.disabled = true;
}

enable() {
this.disabled = false;
}

getFilename(key, ext) {
return path.join(this.dir, key) + ext;
}

has(key, ext) {
if (this.disabled) {
return false;
}

return fs.existsSync(this.getFilename(key, ext));
}

get(key, ext) {
if (!this.has(key, ext)) {
return undefined;
}
if (ext === '.json') {
return readJson(this.getFilename(key, ext));
} else {
return readFile(this.getFilename(key, ext));
}
}

write(key, ext, content) {
if (this.disabled) {
return;
}
const filename = this.getFilename(key, ext);
if (ext === '.json') {
return writeJson(filename, content);
} else {
return writeFile(filename, content);
}
}

wrapAsync(original, identity) {
const cache = this;
return async function(...args) {
const { key, ext } = identity.call(this, ...args);
const cached = cache.get(key, ext);
if (cached) {
return cached;
}
const result = await original.call(this, ...args);
cache.write(key, ext, result);
return result;
};
}

wrapNormal(original, identity) {
const cache = this;
return function(...args) {
const { key, ext } = identity.call(this, ...args);
const cached = cache.get(key, ext);
if (cached) {
return cached;
}
const result = original.call(this, ...args);
cache.write(key, ext, result);
return result;
};
}

wrap(Class, identities) {
for (let method of Object.keys(identities)) {
const original = Class.prototype[method];
const identity = identities[method];
this.originals[method] = original;
if (isAsync(original)) {
Class.prototype[method] = this.wrapAsync(original, identity);
} else {
Class.prototype[method] = this.wrapNormal(original, identity);
}
}
}
}

module.exports = Cache;
Loading