Comparing version 0.9.0 to 0.10.0
@@ -11,3 +11,3 @@ 'use strict'; | ||
execute (argv) { | ||
const config = loadConfig(argv.c || argv.config); | ||
const config = loadConfig(argv.config); | ||
const opts = options.get(argv); | ||
@@ -18,15 +18,7 @@ return execute.checkRules(config, opts).then(logErrors(opts), console.error); | ||
type: 'object', | ||
oneOf: [ | ||
{ required: ['c'] }, | ||
{ required: ['config'] } | ||
], | ||
required: ['config'], | ||
properties: { | ||
c: { type: 'string' }, | ||
config: { type: 'string' }, | ||
u: { type: 'string' }, | ||
p: { type: 'string' }, | ||
user: { type: 'string' }, | ||
pass: { type: 'string' }, | ||
a: { $ref: '#/definitions/dateTimeOrInt' }, | ||
b: { $ref: '#/definitions/dateTimeOrInt' }, | ||
after: { $ref: '#/definitions/dateTimeOrInt' }, | ||
@@ -36,3 +28,11 @@ before: { $ref: '#/definitions/dateTimeOrInt' }, | ||
until: { $ref: '#/definitions/dateTimeOrInt' }, | ||
tap: { type: 'boolean' } | ||
tap: { type: 'boolean' }, | ||
teamAccess: { | ||
type: 'string', | ||
enum: ['admin', 'write', 'read'], | ||
default: 'admin' | ||
} | ||
}, | ||
patternProperties: { | ||
'^(c|u|p|a|b|team-access)$': true | ||
} | ||
@@ -39,0 +39,0 @@ } |
@@ -7,3 +7,4 @@ 'use strict'; | ||
coerceTypes: true, | ||
jsonPointers: true | ||
jsonPointers: true, | ||
useDefaults: true | ||
}); | ||
@@ -52,18 +53,8 @@ | ||
get (argv) { | ||
const opts = {}; | ||
const user = argv.u || argv.user; | ||
const pass = argv.p || argv.pass; | ||
if (user || pass) opts.auth = { user, pass }; | ||
const after = argv.a || argv.after; | ||
const before = argv.b || argv.before; | ||
if (after) opts.after = getRangeDate(after); | ||
if (before) opts.before = getRangeDate(before); | ||
opts.commits = {since: getRangeDate(argv.since || 30)}; | ||
if (argv.until) opts.commits.until = getRangeDate(argv.until); | ||
opts.tap = argv.tap; | ||
get ({user, pass, after, before, since, until, tap, teamAccess}) { | ||
const opts = { | ||
after, before, tap, teamAccess, | ||
commits: {since, until} | ||
}; | ||
if (user || pass) opts.auth = {user, pass}; | ||
return opts; | ||
@@ -77,6 +68,1 @@ } | ||
} | ||
function getRangeDate(param) { | ||
return new Date(typeof param == 'string' ? param | ||
: Date.now() - param * 86400000); // days | ||
} |
@@ -8,3 +8,12 @@ 'use strict'; | ||
module.exports = (_argv, doExit) => { | ||
const argv = minimist(_argv); | ||
const argv = minimist(_argv, { | ||
alias: { | ||
config: 'c', | ||
user: 'u', | ||
pass: 'p', | ||
after: 'a', | ||
before: 'b', | ||
teamAccess: 'team-access' | ||
} | ||
}); | ||
const command = argv._[0] || 'check'; | ||
@@ -11,0 +20,0 @@ const cmd = commands[command]; |
@@ -8,2 +8,7 @@ 'use strict'; | ||
let options = {}; | ||
const teamPermissions = { | ||
admin: 'admin', | ||
write: 'push', | ||
read: 'pull' | ||
}; | ||
@@ -69,3 +74,3 @@ module.exports = github; | ||
}, | ||
async team (org, teamName) { | ||
async team (org, teamName, teamAccess='admin') { | ||
const teams = await github.getTeams(org); | ||
@@ -75,3 +80,4 @@ const team = teams.find(t => t.name == teamName); | ||
const repos = await allPages(`/teams/${team.id}/repos`); | ||
return repos.filter((r) => !r.fork); | ||
const perm = teamPermissions[teamAccess]; | ||
return repos.filter((r) => !r.fork && r.permissions[perm]); | ||
} | ||
@@ -78,0 +84,0 @@ }, |
@@ -22,2 +22,3 @@ 'use strict'; | ||
let CORE_LOADED = false; | ||
const MS_PER_DAY = 86400000; | ||
@@ -27,5 +28,4 @@ | ||
async checkRules (config, options={}) { | ||
if (!CORE_LOADED) loadCoreRules(); | ||
if (config.plugins) loadPlugins(config); | ||
config = execute.prepareConfig(config); | ||
options = prepareOptions(options); | ||
github.setOptions(options); | ||
@@ -89,3 +89,3 @@ const repoSourceRules = await execute.prepareRepoRules(config, options); | ||
} | ||
const repos = await github.getRepos.team(orgName, shortTeamName); | ||
const repos = await github.getRepos.team(orgName, shortTeamName, options.teamAccess); | ||
addRepos(repos, teams[teamName].rules); | ||
@@ -172,2 +172,4 @@ } | ||
prepareConfig(config) { | ||
if (!CORE_LOADED) loadCoreRules(); | ||
if (config.plugins) loadPlugins(config); | ||
config = JSON.parse(JSON.stringify(config)); | ||
@@ -218,2 +220,7 @@ callValidate(ajv.getSchema('config'), config, 'config'); | ||
} | ||
}, | ||
dateDaysAgo(days) { | ||
let ts = Date.now() - days * MS_PER_DAY; | ||
return new Date(ts - ts % MS_PER_DAY); | ||
} | ||
@@ -250,2 +257,33 @@ }; | ||
function prepareOptions(options) { | ||
options = copy(options); | ||
const {after, before, teamAccess} = options; | ||
if (after) options.after = getRangeDate(after, 'after'); | ||
if (before) options.before = getRangeDate(before, 'before'); | ||
if (options.commits === undefined) { | ||
options.commits = {since: execute.dateDaysAgo(30)}; | ||
} else { | ||
options.commits = copy(options.commits); | ||
const {since, until} = options.commits; | ||
if (since) options.commits.since = getRangeDate(since, 'commits.since'); | ||
if (until) options.commits.until = getRangeDate(until, 'commits.until'); | ||
} | ||
if (!teamAccess) options.teamAccess = 'admin'; | ||
return options; | ||
} | ||
function getRangeDate(param, name) { | ||
switch (typeof param) { | ||
case 'string': | ||
return param; | ||
case 'number': | ||
return execute.dateDaysAgo(param); | ||
default: | ||
if (param instanceof Date) return param; | ||
throw new Error(`incorrect "${name}" option type: ${param.toString()}`); | ||
} | ||
} | ||
function callValidate(validate, data, title) { | ||
@@ -252,0 +290,0 @@ if (!validate(data)) |
@@ -11,27 +11,46 @@ 'use strict'; | ||
schema: {}, | ||
schema: { | ||
type: 'object', | ||
properties: { | ||
branches: { | ||
type: 'array', | ||
items: {type: 'string'}, | ||
uniqueItems: true, | ||
default: ['master'] | ||
} | ||
} | ||
}, | ||
source: 'branches', | ||
check: { | ||
type: 'array', | ||
contains: { | ||
type: 'object', | ||
required: ['name', 'protected'], | ||
properties: { | ||
name: {const: 'master'}, | ||
protected: {const: true} | ||
} | ||
async check(cfg, repoBranches, orgRepo, github) { | ||
const brs = []; | ||
for (const branch of cfg.branches) { | ||
if (repoBranches.findIndex(b => b.name == branch) == -1) continue; | ||
const branchUrl = `/repos/${orgRepo}/branches/${branch}`; | ||
const branchMeta = await github.get(branchUrl); | ||
if (!branchMeta.protected) brs.push(branchMeta); | ||
} | ||
if (brs.length == 0) return {valid: true}; | ||
const allBranches = brs.map(b => b.name).join(', '); | ||
return { | ||
valid: false, | ||
message: `Unprotected branch${plural(brs)}: ${allBranches}`, | ||
messages: brs.map(b => `Unprotected branch ${b.name}`) | ||
}; | ||
function plural(arr) { | ||
return arr.length > 1 ? 'es' : ''; | ||
} | ||
}, | ||
issue: { | ||
title: 'Master branch is not protected', | ||
title: 'Branches are not protected', | ||
comments: { | ||
create: 'Please enable master protection', | ||
update: 'Reminder: please enable master protection', | ||
close: 'Master protection enabled, closing', | ||
reopen: 'Please enable master protection' | ||
create: 'Please enable branch protection', | ||
close: 'Branch protection enabled, closing', | ||
reopen: 'Branches are not protected, probably removed. Please fix it' | ||
} | ||
} | ||
}; |
'use strict'; | ||
const PERMISSIONS = { | ||
admin: 2, | ||
write: 1, | ||
read: 0 | ||
}; | ||
module.exports = { | ||
@@ -19,2 +25,7 @@ meta: { | ||
items: {type: 'string'} | ||
}, | ||
minPermission: { | ||
type: 'string', | ||
enum: ['admin', 'write', 'read'], | ||
default: 'admin' | ||
} | ||
@@ -27,4 +38,7 @@ } | ||
check(cfg, repoTeams) { | ||
for (const team of repoTeams) | ||
if (cfg.teams.indexOf(team.name) >= 0) return {valid: true}; | ||
for (const team of repoTeams) { | ||
const assigned = PERMISSIONS[team.permission] >= PERMISSIONS[cfg.minPermission] | ||
&& cfg.teams.indexOf(team.name) >= 0; | ||
if (assigned) return {valid: true}; | ||
} | ||
@@ -31,0 +45,0 @@ return { |
{ | ||
"name": "gh-lint", | ||
"version": "0.9.0", | ||
"version": "0.10.0", | ||
"description": "Rule-based command-line tool for auditing GitHub repositories", | ||
@@ -5,0 +5,0 @@ "main": "lib/execute/index.js", |
@@ -92,2 +92,3 @@ # gh-lint | ||
- `--tap` - output results in TAP format | ||
- `--team-access` - team access level required for repo to be associated with the team (for team-specific rules). The default is "admin". Other values are "write" (includes admin access) and "read" (repo will be associated with the team that has any access level). | ||
@@ -94,0 +95,0 @@ |
@@ -89,6 +89,6 @@ 'use strict'; | ||
it('should execute rules for all repos of a team', () => { | ||
it('should execute rules for all repos assigned to a team with admin permission only', () => { | ||
githubMock.teams(); | ||
githubMock.repos.team.mol_fe.list(); | ||
githubMock.repos.team.mol_fe.meta(); | ||
githubMock.repos.team.mol_fe.meta('admin'); | ||
@@ -103,2 +103,17 @@ const config = require('../fixtures/config-teams.json'); | ||
}); | ||
it('should execute rules for all repos assigned to a team with any permission level', () => { | ||
githubMock.teams(); | ||
githubMock.repos.team.mol_fe.list(); | ||
githubMock.repos.team.mol_fe.meta(); | ||
const config = require('../fixtures/config-teams.json'); | ||
return execute.checkRules(config, {teamAccess: 'read'}) | ||
.then((results) => { | ||
assert.deepStrictEqual(results, require('../fixtures/config-teams_read-access_expected_results.json')); | ||
assert(nock.isDone()); | ||
}); | ||
}); | ||
}); |
@@ -36,2 +36,8 @@ 'use strict'; | ||
.forEach(addRepoMock('milojs', repos)); | ||
}, | ||
branches() { | ||
glob.sync('../fixtures/milojs_repo_branches/*.list.json', { cwd: __dirname }) | ||
.forEach(addBranchesMock('milojs')); | ||
glob.sync('../fixtures/milojs_repo_branches/*.branch.json', { cwd: __dirname }) | ||
.forEach(addBranchMock('milojs')); | ||
} | ||
@@ -45,6 +51,8 @@ } | ||
}, | ||
meta() { | ||
meta(teamPermission) { // 'pull', 'push', 'admin' | ||
const repos = require('../fixtures/molfe_repos.json'); | ||
for (const repo of repos) | ||
mock(`/repos/MailOnline/${repo.name}`, path.join(__dirname, `../fixtures/mailonline_repos/${repo.name}.json`)); | ||
for (const repo of repos) { | ||
if (!teamPermission || repo.permissions[teamPermission]) | ||
mock(`/repos/MailOnline/${repo.name}`, path.join(__dirname, `../fixtures/mailonline_repos/${repo.name}.json`)); | ||
} | ||
} | ||
@@ -92,1 +100,17 @@ } | ||
} | ||
function addBranchesMock(org) { | ||
return function (file) { | ||
const repoName = path.basename(file, '.list.json'); | ||
mock(`/repos/${org}/${repoName}/branches?per_page=30&page=1`, file); | ||
}; | ||
} | ||
function addBranchMock(org) { | ||
return function (file) { | ||
const [repoName, branchName] = path.basename(file, '.branch.json').split('_'); | ||
mock(`/repos/${org}/${repoName}/branches/${branchName}`, file); | ||
}; | ||
} |
@@ -58,3 +58,3 @@ 'use strict'; | ||
describe('.team', () => { | ||
it('should load organization repos', () => { | ||
it('should load team repos only with admin team access', () => { | ||
githubMock.teams(); | ||
@@ -65,2 +65,15 @@ githubMock.repos.team.mol_fe.list(); | ||
.then(result => { | ||
const expectedRepos = require('../fixtures/molfe_repos.json') | ||
.filter(r => r.permissions.admin); | ||
assert.deepStrictEqual(result, expectedRepos); | ||
assert(nock.isDone()); | ||
}); | ||
}); | ||
it('should load team repos with any access', () => { | ||
githubMock.teams(); | ||
githubMock.repos.team.mol_fe.list(); | ||
return github.getRepos.team('MailOnline', '#mol-fe', 'read') | ||
.then(result => { | ||
assert.deepStrictEqual(result, require('../fixtures/molfe_repos.json')); | ||
@@ -67,0 +80,0 @@ assert(nock.isDone()); |
@@ -151,3 +151,3 @@ 'use strict'; | ||
describe('teams scope', () => { | ||
it('should collect rules for repos for team', () => { | ||
it('should collect rules for repos for team with agmin access only', () => { | ||
githubMock.teams(); | ||
@@ -173,2 +173,46 @@ githubMock.repos.team.mol_fe.list(); | ||
}, | ||
'MailOnline/videojs-vast-vpaid': { | ||
meta: { | ||
'repo-description': { mode: 2, minLength: 1 }, | ||
'repo-homepage': { mode: 1, minLength: 1 } | ||
} | ||
}, | ||
'MailOnline/VPAIDFLASHClient': { | ||
meta: { | ||
'repo-description': { mode: 2, minLength: 1 }, | ||
'repo-homepage': { mode: 1, minLength: 1 } | ||
} | ||
}, | ||
'MailOnline/VPAIDHTML5Client': { | ||
meta: { | ||
'repo-description': { mode: 2, minLength: 1 }, | ||
'repo-homepage': { mode: 1, minLength: 1 } | ||
} | ||
} | ||
}); | ||
assert(nock.isDone()); | ||
}); | ||
}); | ||
it('should collect rules for repos for team with any access', () => { | ||
githubMock.teams(); | ||
githubMock.repos.team.mol_fe.list(); | ||
const config = execute.prepareConfig(require('../fixtures/config-teams.json')); | ||
return execute.prepareRepoRules(config, {teamAccess: 'read'}) | ||
.then(repoSourceRules => { | ||
assert.deepStrictEqual(repoSourceRules, { | ||
'MailOnline/eslint-config-mailonline': { | ||
meta: { | ||
'repo-description': { mode: 2, minLength: 1 }, | ||
'repo-homepage': { mode: 1, minLength: 1 } | ||
} | ||
}, | ||
'MailOnline/mol-conventional-changelog': { | ||
meta: { | ||
'repo-description': { mode: 2, minLength: 1 }, | ||
'repo-homepage': { mode: 1, minLength: 1 } | ||
} | ||
}, | ||
'MailOnline/stylelint-config-mailonline': { | ||
@@ -175,0 +219,0 @@ meta: { |
@@ -24,13 +24,2 @@ { | ||
}, | ||
"MailOnline/stylelint-config-mailonline": { | ||
"repo-description": { | ||
"valid": true | ||
}, | ||
"repo-homepage": { | ||
"valid": false, | ||
"message": "not satisfied", | ||
"errors": "data.homepage should NOT be shorter than 10 characters", | ||
"mode": 1 | ||
} | ||
}, | ||
"MailOnline/videojs-vast-vpaid": { | ||
@@ -37,0 +26,0 @@ "repo-description": { |
@@ -54,3 +54,8 @@ { | ||
"repo-team": { | ||
"valid": true | ||
"message": "Repo not assigned to one of specified teams", | ||
"messages": [ | ||
"Specified teams: #ads, #cc, #clj, #ios-ny, #ml-nlp, #mol-fe, #rc, #rta, #support, #systems, Metro, IOS" | ||
], | ||
"mode": 2, | ||
"valid": false | ||
} | ||
@@ -210,3 +215,8 @@ }, | ||
"repo-team": { | ||
"valid": true | ||
"message": "Repo not assigned to one of specified teams", | ||
"messages": [ | ||
"Specified teams: #ads, #cc, #clj, #ios-ny, #ml-nlp, #mol-fe, #rc, #rta, #support, #systems, Metro, IOS" | ||
], | ||
"mode": 2, | ||
"valid": false | ||
} | ||
@@ -213,0 +223,0 @@ }, |
@@ -79,2 +79,31 @@ 'use strict'; | ||
it('should use default since option', () => { | ||
const defaultSinceDate = encodeURIComponent(execute.dateDaysAgo(30).toISOString()); | ||
const apiPath = `/repos/milojs/milo/commits?since=${defaultSinceDate}&per_page=30&page=1`; | ||
nock('https://api.github.com').get(apiPath).reply(200, []); | ||
const config = { | ||
org: 'MailOnline', | ||
repositories: { | ||
'milojs/milo': { | ||
rules: { | ||
'commit-name': 2 | ||
} | ||
} | ||
} | ||
}; | ||
return execute.checkRules(config) | ||
.then((results) => { | ||
assert.deepStrictEqual(results, { | ||
'milojs/milo': { | ||
'commit-name': { | ||
valid: true | ||
} | ||
} | ||
}); | ||
assert(nock.isDone()); | ||
}); | ||
}); | ||
it('should pass if longer message length allowed', () => { | ||
@@ -81,0 +110,0 @@ githubMock.mock('/repos/milojs/milo/commits?since=2017-04-23&per_page=30&page=1', '../fixtures/milojs_milo_commits'); |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
909172
178
17309
107