Skip to content

Commit 57f0d5e

Browse files
authored
Merge pull request #65 from cortex-lab/liveLog
## Added - any number of tasks may be added for a job, which are then executed in series - now serves a Webpage that shows the log in realtime - added a jobs endpoint to see which jobs are on the pile - stderr is piped to log file - flake8 errors are neatly captured in GitHub status description - param to skip checks when only ignored files changed - param to skip draft PR event checks ## Modified - renamed MATLAB-CI to labCI - records endpoint can return pending jobs - tests badge endpoint returns 'error' on errored tests instead of 'unknown' - job waits for coverage calculation and updating of records before finishing - On successful completion of tests the duration is appended to the description
2 parents a95c0f0 + 90c4df6 commit 57f0d5e

File tree

19 files changed

+3594
-2203
lines changed

19 files changed

+3594
-2203
lines changed

CHANGELOG.md

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,30 @@
11
# Changelog
22

3-
## [Latest](https://github.com/cortex-lab/matlab-ci/commits/master) [2.2.1]
3+
## [Latest](https://github.com/cortex-lab/matlab-ci/commits/master) [3.0.0]
4+
5+
## Added
6+
7+
- any number of tasks may be added for a job, which are then executed in series
8+
- now serves a Webpage that shows the log in realtime
9+
- added a jobs endpoint to see which jobs are on the pile
10+
- stderr is piped to log file
11+
- flake8 errors are neatly captured in GitHub status description
12+
- param to skip checks when only ignored files changed
13+
- param to skip draft PR event checks
14+
15+
## Modified
16+
17+
- renamed MATLAB-CI to labCI
18+
- records endpoint can return pending jobs
19+
- tests badge endpoint returns 'error' on errored tests instead of 'unknown'
20+
- job waits for coverage calculation and updating of records before finishing
21+
- On successful completion of tests the duration is appended to the description
22+
23+
## [2.2.1]
424

525
## Modified
626

7-
- fix error where github even incorrectly rejected
27+
- fix error where github event incorrectly rejected
828
- fix bug incorrect log name when endpoint called with branch name
929

1030
## [2.2.0]

README.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,8 @@ Add these to the settings.json file in config:
4040
"pull_request": {
4141
"checks": ["continuous-integration", "coverage"],
4242
"actions": ["opened", "synchronize", "reopened"],
43-
"ref_ignore": ["documentation", "gh-pages"]
43+
"ref_ignore": ["documentation", "gh-pages"],
44+
"files_ignore": [".*\\.yml", ".*\\.md", "LICEN[SC]E"]
4445
}
4546
}
4647
}
@@ -49,6 +50,10 @@ Some extra optional settings:
4950

5051
- `shell` - optional shell to use when calling scripts (see `child_process.execFile` options).
5152
- `events:event:ref_include` - same as `ref_ignore`, but a pass list instead of block list.
53+
- `events:event:files_ignore` - list of files whose changes can be ignored. If only ignored files
54+
are changed checks are skipped.
55+
- `events:pull_request:ignore_drafts` - if true draft pull request actions are skipped (NB: Be
56+
sure to add 'ready_for_review' to the actions list when ignoring drafts).
5257
- `kill_children` - if present and true, `tree-kill` is used to kill the child processes, required
5358
if shell/batch script forks test process (e.g. a batch script calls python).
5459
- `repos` - an array of submodules or map of modules to their corresponding paths.

config/config.js

Lines changed: 35 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -2,78 +2,78 @@ const userSettings = require('./settings.json') || {}; // User settings
22
const path = require('path');
33
env = process.env.NODE_ENV || 'production';
44
const appdata = process.env.APPDATA || process.env.HOME;
5-
const dataPath = process.env.APPDATA? path.join(appdata, 'CI') : path.join(appdata, '.ci');
5+
const dataPath = process.env.APPDATA ? path.join(appdata, 'CI') : path.join(appdata, '.ci');
66
const fixtureDir = path.resolve(__dirname, '..', 'test', 'fixtures');
77
const dbFilename = '.db.json';
88
let settings;
99

1010
// Defaults for when there's no user file; will almost certainly fail
11-
defaults = {
12-
setup_function: null,
13-
test_function: null,
11+
const defaults = {
12+
max_description_len: 140, // GitHub status API has a description char limit
1413
listen_port: 3000,
15-
timeout: 8*60000,
16-
program: "python",
14+
timeout: 8 * 60000,
1715
strict_coverage: false,
1816
events: {
1917
push: {
2018
checks: null,
21-
ref_ignore: ["documentation", "gh-pages"]
19+
ref_ignore: ['documentation', 'gh-pages']
2220
},
2321
pull_request: {
24-
checks: ["continuous-integration", "coverage"],
25-
actions: ["opened", "synchronize", "reopen"],
26-
ref_ignore: ["documentation", "gh-pages"]
22+
checks: ['continuous-integration', 'coverage'],
23+
actions: ['opened', 'synchronize', 'reopen'],
24+
ref_ignore: ['documentation', 'gh-pages']
2725
}
2826
},
2927
dataPath: dataPath,
3028
dbFile: path.join(dataPath, dbFilename)
31-
}
29+
};
3230

3331
// Settings for the tests
34-
testing = {
32+
const testing = {
3533
listen_port: 3000,
3634
timeout: 60000,
37-
setup_function: 'prep_env.BAT',
38-
test_function: "run_tests.BAT",
3935
events: {
4036
push: {
41-
checks: "continuous-integration",
42-
ref_ignore: "documentation"
37+
checks: 'continuous-integration',
38+
ref_ignore: 'documentation'
4339
},
4440
pull_request: {
45-
checks: ["coverage", "continuous-integration"],
46-
actions: ["opened", "synchronize"],
47-
ref_ignore: ["documentation", "gh-pages"]
41+
checks: ['coverage', 'continuous-integration'],
42+
actions: ['opened', 'synchronize'],
43+
ref_ignore: ['documentation', 'gh-pages']
4844
}
4945
},
46+
routines: {
47+
'*': ['prep_env.BAT', 'run_tests.BAT']
48+
},
5049
dataPath: fixtureDir,
5150
dbFile: path.join(fixtureDir, dbFilename) // cache of test results
52-
}
51+
};
5352

5453
// Pick the settings to return
5554
if (env.startsWith('test')) {
56-
settings = testing;
55+
settings = testing;
5756
} else if (userSettings) {
58-
settings = userSettings;
59-
if (!('dbFile' in settings)) {
60-
settings.dbFile = path.join(dataPath, dbFilename)
61-
}
62-
if (!('dataPath' in settings)) {
63-
settings.dataPath = dataPath;
64-
}
57+
settings = userSettings;
6558
} else {
66-
settings = defaults;
59+
settings = defaults;
60+
}
61+
62+
// Ensure defaults for absent fields
63+
for (let field in defaults) {
64+
if (!(field in settings)) settings[field] = defaults[field];
6765
}
6866

6967
// Check ENV set up correctly
7068
required = ['GITHUB_PRIVATE_KEY', 'GITHUB_APP_IDENTIFIER', 'GITHUB_WEBHOOK_SECRET',
71-
'WEBHOOK_PROXY_URL', 'REPO_PATH', 'REPO_NAME', 'REPO_OWNER', 'TUNNEL_HOST',
72-
'TUNNEL_SUBDOMAIN'];
73-
missing = required.filter(o => { return !process.env[o] });
69+
'WEBHOOK_PROXY_URL', 'REPO_PATH', 'REPO_NAME', 'REPO_OWNER', 'TUNNEL_HOST',
70+
'TUNNEL_SUBDOMAIN'];
71+
missing = required.filter(o => {
72+
return !process.env[o];
73+
});
7474
if (missing.length > 0) {
75-
errMsg = `Env not set correctly; the following variables not found: \n${missing.join(', ')}`
76-
throw ReferenceError(errMsg)
75+
errMsg = `Env not set correctly; the following variables not found: \n${missing.join(', ')}`;
76+
throw ReferenceError(errMsg);
7777
}
7878

79-
module.exports = { settings }
79+
module.exports = { settings };

config/settings.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
11
{
2-
"setup_function": "prep_env.BAT",
3-
"test_function": "run_tests.BAT",
42
"listen_port": 3000,
53
"timeout": 480000,
6-
"program": "python",
74
"strict_coverage": false,
85
"events": {
96
"push": {
@@ -15,5 +12,8 @@
1512
"actions": ["opened", "synchronize", "reopened"],
1613
"ref_ignore": ["documentation", "gh-pages"]
1714
}
15+
},
16+
"routines": {
17+
"*": ["prep_env.BAT", "run_tests.BAT"]
1818
}
1919
}

coverage.js

Lines changed: 68 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,9 @@
2121
const fs = require('fs'),
2222
xml2js = require('xml2js'),
2323
crypto = require('crypto'),
24-
assert = require('assert').strict,
2524
parser = new xml2js.Parser(),
2625
path = require('path');
27-
var timestamp, cb;
26+
var timestamp;
2827

2928
var token = process.env.COVERALLS_TOKEN;
3029

@@ -33,14 +32,14 @@ var token = process.env.COVERALLS_TOKEN;
3332
* Loads file containing source code, returns a hash and line count
3433
* @param {String} path - Path to the source code file.
3534
* @returns {Object} key `Hash` contains MD5 digest string of file; `count` contains number of lines in source file
36-
* @todo Make asynchronous
3735
*/
3836
function md5(path) {
39-
var hash = crypto.createHash('md5'); // Creating hash object
40-
var buf = fs.readFileSync(path, 'utf-8'); // Read in file
41-
var count = buf.split(/\r\n|\r|\n/).length; // Count the number of lines
42-
hash.update(buf, 'utf-8'); // Update hash
43-
return {hash: hash.digest('hex'), count: count};
37+
const hash = crypto.createHash('md5'); // Creating hash object
38+
const buf = fs.readFileSync(path, 'utf-8'); // Read in file
39+
const count = buf.split(/\r\n|\r|\n/).length; // Count the number of lines
40+
hash.update(buf, 'utf-8'); // Update hash
41+
42+
return {hash: hash.digest('hex'), count: count};
4443
}
4544

4645

@@ -50,42 +49,41 @@ function md5(path) {
5049
* @param {Array} classList - An array of class objects from the loaded XML file.
5150
* @param {String} srcPath - The root path of the code repository.
5251
* @param {String} sha - The commit SHA for this coverage test.
53-
* @param {function} callback - The callback function to run when complete. Takes object containing array of source
54-
* code files and their code coverage
5552
* @returns {Object}
5653
* @todo Generalize path default
57-
* @fixme Doesn't work with python's coverage
5854
*/
59-
function formatCoverage(classList, srcPath, sha) {
60-
var job = {};
61-
var sourceFiles = [];
62-
var digest;
63-
srcPath = typeof srcPath != "undefined" ? srcPath : process.env.HOMEPATH; // default to home dir
64-
// For each class, create file object containing array of lines covered and add to sourceFile array
65-
classList.forEach( async c => {
66-
let file = {}; // Initialize file object
67-
let fullPath = c.$.filename.startsWith(srcPath)? c.$.filename : path.join(srcPath, c.$.filename);
68-
digest = md5(fullPath); // Create digest and line count for file
69-
let lines = new Array(digest.count).fill(null); // Initialize line array the size of source code file
70-
c.lines[0].line.forEach(ln => {
71-
let n = Number(ln.$.number);
72-
if (n <= digest.count) {lines[n] = Number(ln.$.hits) }
73-
});
74-
// create source file object
75-
file.name = c.$.filename;
76-
file.source_digest = digest.hash;
77-
file.coverage = lines; // file.coverage[0] == line 1
78-
sourceFiles.push(file);
79-
});
55+
async function formatCoverage(classList, srcPath, sha) {
56+
var job = {};
57+
var sourceFiles = [];
58+
var digest;
59+
srcPath = typeof srcPath != 'undefined' ? srcPath : process.env.REPO_PATH; // default to home dir
60+
// For each class, create file object containing array of lines covered and add to sourceFile array
61+
await Promise.all(classList.map(async c => {
62+
let file = {}; // Initialize file object
63+
let fullPath = c.$.filename.startsWith(srcPath) ? c.$.filename : path.join(srcPath, c.$.filename);
64+
digest = md5(fullPath); // Create digest and line count for file
65+
let lines = new Array(digest.count).fill(null); // Initialize line array the size of source code file
66+
c.lines[0].line.forEach(ln => {
67+
let n = Number(ln.$.number);
68+
if (n <= digest.count) {
69+
lines[n] = Number(ln.$.hits);
70+
}
71+
});
72+
// create source file object
73+
file.name = c.$.filename;
74+
file.source_digest = digest.hash;
75+
file.coverage = lines; // file.coverage[0] == line 1
76+
sourceFiles.push(file);
77+
}));
8078

81-
job.repo_token = token; // env secret token?
82-
job.service_name = `coverage/${process.env.USERDOMAIN}`;
83-
// The associated pull request ID of the build. Used for updating the status and/or commenting.
84-
job.service_pull_request = '';
85-
job.source_files = sourceFiles;
86-
job.commit_sha = sha;
87-
job.run_at = timestamp; // "2013-02-18 00:52:48 -0800"
88-
cb(job);
79+
job.repo_token = token; // env secret token
80+
job.service_name = `coverage/${process.env.USERDOMAIN}`;
81+
// The associated pull request ID of the build. Used for updating the status and/or commenting.
82+
job.service_pull_request = '';
83+
job.source_files = sourceFiles;
84+
job.commit_sha = sha;
85+
job.run_at = timestamp; // "2013-02-18 00:52:48 -0800"
86+
return job;
8987
}
9088

9189
/**
@@ -95,44 +93,42 @@ function formatCoverage(classList, srcPath, sha) {
9593
* @param {String} sha - The commit SHA for this coverage test
9694
* @param {String} repo - The repo to which the commit belongs
9795
* @param {Array} submodules - A list of submodules for separating coverage into
98-
* @param {function} callback - The callback function to run when complete
9996
* @see {@link https://github.com/cobertura/cobertura/wiki|Cobertura Wiki}
10097
*/
101-
function coverage(path, repo, sha, submodules, callback) {
102-
cb = callback; // @fixme Making callback global feels hacky
103-
fs.readFile(path, function(err, data) { // Read in XML file
104-
if (err) {throw err} // @fixme deal with file not found errors
105-
parser.parseString(data, function (err, result) { // Parse XML
106-
// Extract root code path
107-
const rootPath = (result.coverage.sources[0].source[0] || process.env.REPO_PATH).replace(/[\/|\\]+$/, '')
108-
assert(rootPath.endsWith(process.env.REPO_NAME), 'Incorrect source code repository')
109-
timestamp = new Date(result.coverage.$.timestamp*1000); // Convert UNIX timestamp to Date object
110-
let classes = []; // Initialize classes array
98+
function coverage(path, repo, sha, submodules) {
99+
return fs.promises.readFile(path) // Read in XML file
100+
.then(parser.parseStringPromise) // Parse XML
101+
.then(result => {
102+
// Extract root code path
103+
const rootPath = (result.coverage.sources[0].source[0] || process.env.REPO_PATH)
104+
.replace(/[\/|\\]+$/, '');
105+
timestamp = new Date(result.coverage.$.timestamp * 1000); // Convert UNIX timestamp to Date object
106+
let classes = []; // Initialize classes array
111107

112-
const packages = result.coverage.packages[0].package;
113-
packages.forEach(pkg => { classes.push(pkg.classes[0].class) }); // Get all classes
114-
classes = classes.reduce((acc, val) => acc.concat(val), []); // Flatten
108+
const packages = result.coverage.packages[0].package;
109+
packages.forEach(pkg => { classes.push(pkg.classes[0].class); }); // Get all classes
110+
classes = classes.reduce((acc, val) => acc.concat(val), []); // Flatten
115111

116-
// The submodules
117-
const byModule = {'main' : []};
118-
submodules.forEach((x) => { byModule[x] = []; }); // initialize submodules
112+
// The submodules
113+
const byModule = {'main': []};
114+
submodules.forEach((x) => { byModule[x] = []; }); // initialize submodules
119115

120-
// Sort into piles
121-
byModule['main'] = classes.filter(function (e) {
122-
if (e.$.filename.search(/(tests\\|_.*test|docs\\)/i) !== -1) {return false;} // Filter out tests and docs
123-
if (!Array.isArray(e.lines[0].line)) {return false;} // Filter out files with no functional lines
124-
for (let submodule of submodules) {
125-
if (e.$.filename.startsWith(submodule)) {
126-
byModule[submodule].push(e); return false;
127-
}
128-
}
129-
return true;
116+
// Sort into piles
117+
byModule['main'] = classes.filter(function (e) {
118+
if (e.$.filename.search(/(tests\\|_.*test|docs\\)/i) !== -1) return false; // Filter out tests and docs
119+
if (!Array.isArray(e.lines[0].line)) return false; // Filter out files with no functional lines
120+
for (let submodule of submodules) {
121+
if (e.$.filename.startsWith(submodule)) {
122+
byModule[submodule].push(e);
123+
return false;
124+
}
125+
}
126+
return true;
127+
});
128+
// Select module
129+
let modules = byModule[repo] || byModule['main'];
130+
return formatCoverage(modules, rootPath, sha);
130131
});
131-
// Select module
132-
let modules = byModule[repo] || byModule['main'];
133-
formatCoverage(modules, rootPath, callback);
134-
});
135-
});
136132
}
137133

138134

0 commit comments

Comments
 (0)