Skip to content

Commit e5ac4f0

Browse files
committed
ci: split CI aggregator and generates markdown
1 parent 12a7909 commit e5ac4f0

File tree

3 files changed

+170
-73
lines changed

3 files changed

+170
-73
lines changed

bin/ncu-ci

+37-72
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,6 @@
22

33
'use strict';
44

5-
const {
6-
parsePRFromURL
7-
} = require('../lib/links');
8-
95
const {
106
JobParser,
117
parseJobFromURL,
@@ -15,7 +11,7 @@ const {
1511
} = require('../lib/ci/ci_type_parser');
1612

1713
const {
18-
PRBuild, BenchmarkRun, CommitBuild, listBuilds
14+
PRBuild, BenchmarkRun, CommitBuild, listBuilds, FailureAggregator
1915
// , jobCache
2016
} = require('../lib/ci/ci_result_parser');
2117
const clipboardy = require('clipboardy');
@@ -26,8 +22,6 @@ const auth = require('../lib/auth');
2622
const Request = require('../lib/request');
2723
const CLI = require('../lib/cli');
2824
const yargs = require('yargs');
29-
const _ = require('lodash');
30-
const chalk = require('chalk');
3125

3226
// This is used for testing
3327
// Default cache dir is ${ncu-source-dir}/.ncu/cache
@@ -49,12 +43,20 @@ const argv = yargs
4943
})
5044
.command({
5145
command: 'walk <type>',
52-
desc: 'Walk the CI and store the failures',
46+
desc: 'Walk the CI and display the failures',
5347
builder: (yargs) => {
5448
yargs
5549
.positional('type', {
5650
describe: 'type of CI',
5751
choices: ['commit', 'pr']
52+
})
53+
.option('stats', {
54+
default: false,
55+
describe: 'Aggregate the results'
56+
})
57+
.option('limit', {
58+
default: 99,
59+
describe: 'Maximum number of CIs to get data from'
5860
});
5961
},
6062
handler
@@ -160,19 +162,10 @@ async function runQueue(queue, cli, request, argv) {
160162
dataToJson = dataToJson.concat(build.formatAsJson());
161163
}
162164

163-
if (argv.copy) {
164-
clipboardy.writeSync(dataToCopy);
165-
cli.separator('');
166-
cli.log(`Written markdown to clipboard`);
167-
}
168-
169-
if (argv.json) {
170-
writeJson(argv.json, dataToJson);
171-
cli.separator('');
172-
cli.log(`Written JSON to ${argv.json}`);
173-
}
174-
175-
return dataToJson;
165+
return {
166+
json: dataToJson,
167+
copy: dataToCopy
168+
};
176169
}
177170

178171
function pad(any, length) {
@@ -199,55 +192,6 @@ function displayHealth(builds, cli) {
199192
cli.log(result);
200193
}
201194

202-
function getHighlight(f) {
203-
return f.reason.split('\n')[f.highlight]
204-
.replace(/not ok \d+ /, '')
205-
.replace(
206-
/'JNLP4-connect connection from .+?'/, 'JNLP4-connect connection from ...'
207-
)
208-
.replace(/FATAL: Could not checkout \w+/, 'FATAL: Could not checkout ...');
209-
}
210-
211-
function aggregateFailures(cli, failures) {
212-
const grouped = _.chain(failures)
213-
.groupBy(getHighlight)
214-
.toPairs()
215-
.sortBy()
216-
.value();
217-
let results = [];
218-
for (const item of grouped) {
219-
const [ key, failures ] = item;
220-
const cleaned = _.chain(failures)
221-
.uniqBy('source')
222-
.sortBy((f) => parseJobFromURL(f.upstream).jobid)
223-
.value();
224-
results.push([ key, failures, cleaned ]);
225-
};
226-
227-
results = _.sortBy(results, r => 0 - (r[2].length));
228-
229-
cli.separator(chalk.bold('Stats'));
230-
for (const item of results) {
231-
const [ key, failures, cleaned ] = item;
232-
const machines = _.uniq(failures.map(f => f.builtOn)).join(', ');
233-
cli.table('Reason', key);
234-
cli.table('Type', failures[0].type);
235-
const prs = cleaned
236-
.map(f => {
237-
const parsed = parsePRFromURL(f.source);
238-
return parsed ? `#${parsed.prid}` : f.source;
239-
})
240-
.join(', ');
241-
cli.table('Failed PR', `${cleaned.length} (${prs})`);
242-
cli.table('Appeared', machines);
243-
if (cleaned.length > 1) {
244-
cli.table('First CI', `${cleaned[0].upstream}`);
245-
}
246-
cli.table('Last CI', `${cleaned[cleaned.length - 1].upstream}`);
247-
cli.separator();
248-
}
249-
}
250-
251195
async function main(command, argv) {
252196
const cli = new CLI();
253197
const credentials = await auth({
@@ -267,7 +211,7 @@ async function main(command, argv) {
267211
const type = commandToType[argv.type];
268212
const builds = await listBuilds(cli, request, type);
269213
if (command === 'walk') {
270-
for (const build of builds.failed) {
214+
for (const build of builds.failed.slice(0, argv.limit)) {
271215
queue.push(build);
272216
}
273217
} else {
@@ -304,8 +248,29 @@ async function main(command, argv) {
304248

305249
if (queue.length > 0) {
306250
const data = await runQueue(queue, cli, request, argv);
251+
307252
if (command === 'walk' && argv.stats) {
308-
aggregateFailures(cli, data);
253+
const aggregator = new FailureAggregator(cli, data.json);
254+
data.json = aggregator.aggregate();
255+
cli.log('');
256+
cli.separator('Stats');
257+
cli.log('');
258+
aggregator.display();
259+
if (argv.copy) {
260+
data.copy = aggregator.formatAsMarkdown();
261+
}
262+
}
263+
264+
if (argv.copy) {
265+
clipboardy.writeSync(data.copy);
266+
cli.separator('');
267+
cli.log(`Written markdown to clipboard`);
268+
}
269+
270+
if (argv.json) {
271+
writeJson(argv.json, data.json);
272+
cli.separator('');
273+
cli.log(`Written JSON to ${argv.json}`);
309274
}
310275
}
311276
}

lib/ci/ci_failure_parser.js

+7
Original file line numberDiff line numberDiff line change
@@ -250,4 +250,11 @@ CIFailureParser.FAILURE_CONSTRUCTORS = {
250250
GIT_FAILURE: GitFailure
251251
};
252252
CIFailureParser.CIResult = CIResult;
253+
CIFailureParser.FAILURE_TYPES_NAME = {
254+
BUILD_FAILURE: 'Build Failure',
255+
JENKINS_FAILURE: 'Jenkins Failure',
256+
JS_TEST_FAILURE: 'JSTest Failure',
257+
CC_TEST_FAILURE: 'CCTest Failure',
258+
GIT_FAILURE: 'Git Failure'
259+
};
253260
module.exports = CIFailureParser;

lib/ci/ci_result_parser.js

+126-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
'use strict';
22

3+
const {
4+
parsePRFromURL
5+
} = require('../links');
36
const Cache = require('../cache');
47
const CIFailureParser = require('./ci_failure_parser');
58
const {
@@ -9,14 +12,16 @@ const {
912
FAILURE_CONSTRUCTORS: {
1013
[BUILD_FAILURE]: BuildFailure
1114
},
12-
CIResult
15+
CIResult,
16+
FAILURE_TYPES_NAME
1317
} = CIFailureParser;
1418
const {
1519
CI_DOMAIN,
1620
parseJobFromURL,
1721
CI_TYPES
1822
} = require('./ci_type_parser');
1923
const qs = require('querystring');
24+
const _ = require('lodash');
2025
const chalk = require('chalk');
2126

2227
const SUCCESS = 'SUCCESS';
@@ -325,6 +330,125 @@ class TestBuild extends Job {
325330
}
326331
}
327332

333+
function getHighlight(f) {
334+
return f.reason.split('\n')[f.highlight]
335+
.replace(/not ok \d+ /, '')
336+
.replace(
337+
/'JNLP4-connect connection from .+?'/, 'JNLP4-connect connection from ...'
338+
)
339+
.replace(/FATAL: Could not checkout \w+/, 'FATAL: Could not checkout ...');
340+
}
341+
342+
function markdownRow(...args) {
343+
let result = '';
344+
for (const item of args) {
345+
result += `| ${item} `;
346+
}
347+
return result + '|\n';
348+
}
349+
350+
class FailureAggregator {
351+
constructor(cli, failures) {
352+
this.cli = cli;
353+
this.failures = failures;
354+
this.aggregates = null;
355+
}
356+
357+
aggregate() {
358+
const failures = this.failures;
359+
const groupedByReason = _.chain(failures)
360+
.groupBy(getHighlight)
361+
.toPairs()
362+
.sortBy(0)
363+
.value();
364+
const data = [];
365+
for (const item of groupedByReason) {
366+
const [ reason, failures ] = item;
367+
// If multiple sub builds of one PR are failed by the same reason,
368+
// we'll only take one of those builds, as that might be a genuine failure
369+
const prs = _.chain(failures)
370+
.uniqBy('source')
371+
.sortBy((f) => parseJobFromURL(f.upstream).jobid)
372+
.map((item) => ({ source: item.source, upstream: item.upstream }))
373+
.value();
374+
const machines = _.uniq(failures.map(f => f.builtOn));
375+
data.push({
376+
reason, type: failures[0].type, failures, prs, machines
377+
});
378+
};
379+
380+
const groupedByType = _.groupBy(data, 'type');
381+
for (const type of Object.keys(groupedByType)) {
382+
groupedByType[type] =
383+
_.sortBy(groupedByType[type], r => 0 - (r.prs.length));
384+
}
385+
this.aggregates = groupedByType;
386+
return groupedByType;
387+
}
388+
389+
formatAsMarkdown() {
390+
let { aggregates } = this;
391+
if (!aggregates) {
392+
aggregates = this.aggregates = this.aggregate();
393+
}
394+
395+
let output = '';
396+
for (const type of Object.keys(aggregates)) {
397+
output += `\n### ${FAILURE_TYPES_NAME[type]}\n`;
398+
for (const item of aggregates[type]) {
399+
const { reason, type, prs, failures, machines } = item;
400+
if (prs.length < 2) { continue; }
401+
output += markdownRow('Reason', `\`${reason}\``);
402+
output += markdownRow('-', ':-');
403+
output += markdownRow('Type', type);
404+
const source = prs.map(f => f.source);
405+
output += markdownRow(
406+
'Failed PR', `${source.length} (${source.join(', ')})`
407+
);
408+
output += markdownRow('Appeared', machines.join(', '));
409+
if (prs.length > 1) {
410+
output += markdownRow('First CI', `${prs[0].upstream}`);
411+
}
412+
output += markdownRow('Last CI', `${prs[prs.length - 1].upstream}`);
413+
output += '\n' + fold('Example', failures[0].reason) + '\n';
414+
output += '\n-------\n\n';
415+
}
416+
}
417+
return output;
418+
}
419+
420+
display() {
421+
let { cli, aggregates } = this;
422+
if (!aggregates) {
423+
aggregates = this.aggregates = this.aggregate();
424+
}
425+
426+
for (const type of Object.keys(aggregates)) {
427+
cli.separator(type);
428+
for (const item of aggregates[type]) {
429+
const { reason, type, prs, failures, machines } = item;
430+
cli.table('Reason', reason);
431+
cli.table('Type', type);
432+
const source = prs
433+
.map(f => {
434+
const parsed = parsePRFromURL(f.source);
435+
return parsed ? `#${parsed.prid}` : f.source;
436+
});
437+
cli.table('Failed PR', `${source.length} (${source.join(', ')})`);
438+
cli.table('Appeared', machines.join(', '));
439+
if (prs.length > 1) {
440+
cli.table('First CI', `${prs[0].upstream}`);
441+
}
442+
cli.table('Last CI', `${prs[prs.length - 1].upstream}`);
443+
cli.log('\n' + chalk.bold('Example:') + '\n');
444+
const example = failures[0].reason;
445+
cli.log(example.length > 512 ? example.slice(0, 512) + '...' : example);
446+
cli.separator();
447+
}
448+
}
449+
}
450+
}
451+
328452
class CommitBuild extends TestBuild {
329453
constructor(cli, request, id) {
330454
const path = `job/node-test-commit/${id}/`;
@@ -714,6 +838,7 @@ class BenchmarkRun extends Job {
714838
}
715839

716840
module.exports = {
841+
FailureAggregator,
717842
PRBuild,
718843
BenchmarkRun,
719844
CommitBuild,

0 commit comments

Comments
 (0)