From aec6d86764b0b446e8a3f93a599a1a4a60300c86 Mon Sep 17 00:00:00 2001 From: Christophe Hurpeau Date: Sun, 27 Jan 2019 23:11:35 +0100 Subject: [PATCH] feat: lint pr with regexp rules --- lib/actions/autoAssignPRToCreator.js | 14 ++++ lib/actions/editOpenedPR.js | 19 ++++++ lib/actions/lintPR.js | 34 ++++++++++ lib/context/initTeamSlack.js | 2 + lib/context/repoContext.js | 13 ++-- lib/context/teamContext.js | 4 -- lib/index.js | 53 +++++---------- lib/teamconfigs/christophehurpeau.js | 67 +++++++++++++++++++ lib/teamconfigs/index.js | 4 ++ lib/{teamconfig.js => teamconfigs/ornikar.js} | 21 ++++++ package.json | 4 +- yarn.lock | 34 ++++------ 12 files changed, 194 insertions(+), 75 deletions(-) create mode 100644 lib/actions/autoAssignPRToCreator.js create mode 100644 lib/actions/editOpenedPR.js create mode 100644 lib/actions/lintPR.js create mode 100644 lib/teamconfigs/christophehurpeau.js create mode 100644 lib/teamconfigs/index.js rename lib/{teamconfig.js => teamconfigs/ornikar.js} (80%) diff --git a/lib/actions/autoAssignPRToCreator.js b/lib/actions/autoAssignPRToCreator.js new file mode 100644 index 000000000..bcca94b7d --- /dev/null +++ b/lib/actions/autoAssignPRToCreator.js @@ -0,0 +1,14 @@ +'use strict'; + +exports.autoAssignPRToCreator = (repoContext, context) => { + if (!repoContext.config.autoAssignToCreator) return; + + const pr = context.payload.pull_request; + if (pr.assignees.length !== 0) return; + + return context.github.issues.addAssignees( + context.issue({ + assignees: [pr.user.login], + }) + ); +}; diff --git a/lib/actions/editOpenedPR.js b/lib/actions/editOpenedPR.js new file mode 100644 index 000000000..08a8195bf --- /dev/null +++ b/lib/actions/editOpenedPR.js @@ -0,0 +1,19 @@ +'use strict'; + +const cleanTitle = require('../utils/cleanTitle'); + +exports.editOpenedPR = (repoContext, context) => { + if (!repoContext.config.trimTitle) return; + + const pr = context.payload.pull_request; + const title = cleanTitle(pr.title); + + if (pr.title !== title) { + pr.title = title; + context.github.issues.update( + context.issue({ + title, + }) + ); + } +}; diff --git a/lib/actions/lintPR.js b/lib/actions/lintPR.js new file mode 100644 index 000000000..9fb16da9e --- /dev/null +++ b/lib/actions/lintPR.js @@ -0,0 +1,34 @@ +'use strict'; + +exports.lintPR = (repoContext, context) => { + if (!repoContext.config.prLint) return; + + const repo = context.payload.repository; + const pr = context.payload.pull_request; + + // do not lint pr from forks + if (pr.head.repo.id !== repo.id) return; + + const errorRule = repoContext.config.prLint.title.find( + (rule) => !rule.regExp.test(pr.title) + ); + + const date = new Date(); + + return context.github.checks.create( + context.repo({ + name: 'reviewflow/lint-pr', + head_sha: pr.head.sha, + status: 'completed', + conclusion: errorRule ? 'failure' : 'success', + started_at: date, + completed_at: date, + output: errorRule + ? errorRule.error + : { + title: '✓ Your PR is valid', + summary: '', + }, + }) + ); +}; diff --git a/lib/context/initTeamSlack.js b/lib/context/initTeamSlack.js index 176371126..b6f647170 100644 --- a/lib/context/initTeamSlack.js +++ b/lib/context/initTeamSlack.js @@ -3,6 +3,8 @@ const { WebClient } = require('@slack/client'); const initTeamSlack = async (context, config) => { + if (!config.slackToken) return; + const githubLoginToSlackEmail = { ...config.dev, ...config.design }; const slackClient = new WebClient(config.slackToken); diff --git a/lib/context/repoContext.js b/lib/context/repoContext.js index ad8a6d5c6..d153bb286 100644 --- a/lib/context/repoContext.js +++ b/lib/context/repoContext.js @@ -1,6 +1,6 @@ 'use strict'; -const config = require('../teamconfig'); +const teamConfigs = require('../teamconfigs'); const { obtainTeamContext } = require('./teamContext'); const initRepoLabels = require('./initRepoLabels'); @@ -26,11 +26,6 @@ const initRepoContext = async (context, config) => { const addStatusCheck = (context, statusInfo) => { const pr = context.payload.pull_request; - context.log.info('addStatusCheck', { - head_sha: pr.head.sha, - statusInfo, - }); - return context.github.checks.create( context.repo({ name: 'reviewflow', @@ -163,8 +158,8 @@ const repoContexts = new Map(); exports.obtainRepoContext = (context) => { const owner = context.payload.repository.owner; - if (owner.login !== 'ornikar') { - console.warn(owner.login); + if (!teamConfigs[owner.login]) { + console.warn(owner.login, Object.keys(teamConfigs)); return null; } const key = context.payload.repository.id; @@ -175,7 +170,7 @@ exports.obtainRepoContext = (context) => { const existingPromise = repoContextsPromise.get(key); if (existingPromise) return Promise.resolve(existingPromise); - const promise = initRepoContext(context, config); + const promise = initRepoContext(context, teamConfigs[owner.login]); repoContextsPromise.set(key, promise); return promise.then((repoContext) => { diff --git a/lib/context/teamContext.js b/lib/context/teamContext.js index e20615aca..fdb537f3f 100644 --- a/lib/context/teamContext.js +++ b/lib/context/teamContext.js @@ -67,10 +67,6 @@ const teamContexts = new Map(); exports.obtainTeamContext = (context, config) => { const owner = context.payload.repository.owner; - if (owner.login !== 'ornikar') { - console.warn(owner.login); - return null; - } const existingTeamContext = teamContexts.get(owner.login); if (existingTeamContext) return existingTeamContext; diff --git a/lib/index.js b/lib/index.js index 4ce5f96e6..21a547f68 100644 --- a/lib/index.js +++ b/lib/index.js @@ -3,8 +3,9 @@ 'use strict'; const { obtainRepoContext } = require('./context/repoContext'); -const cleanTitle = require('./utils/cleanTitle'); -const config = require('./teamconfig'); +const { autoAssignPRToCreator } = require('./actions/autoAssignPRToCreator'); +const { editOpenedPR } = require('./actions/editOpenedPR'); +const { lintPR } = require('./actions/lintPR'); // const getConfig = require('probot-config') // const { MongoClient } = require('mongodb'); @@ -19,41 +20,14 @@ const config = require('./teamconfig'); * @param {import('probot').Application} app - Probot's Application class. */ module.exports = (app) => { - const autoAssignToCreator = (repoContext, context) => { - if (!repoContext.config.autoAssignToCreator) return; - - const pr = context.payload.pull_request; - if (pr.assignees.length !== 0) return; - - return context.github.issues.addAssignees( - context.issue({ - assignees: [pr.user.login], - }) - ); - }; - - const editOpenedPr = (repoContext, context) => { - if (!repoContext.config.trimTitle) return; - - const pr = context.payload.pull_request; - const title = cleanTitle(pr.title); - - if (pr.title !== title) { - context.github.issues.update( - context.issue({ - title, - }) - ); - } - }; - app.on('pull_request.opened', async (context) => { const repoContext = await obtainRepoContext(context); if (!repoContext) return; await Promise.all([ - autoAssignToCreator(repoContext, context), - editOpenedPr(repoContext, context), + autoAssignPRToCreator(repoContext, context), + editOpenedPR(repoContext, context), + lintPR(repoContext, context), ]); }); @@ -72,7 +46,7 @@ module.exports = (app) => { const shouldWait = false; // repoContext.reviewShouldWait(reviewerGroup, pr.requested_reviewers, { includesWaitForGroups: true }); - if (config.labels.review[reviewerGroup]) { + if (repoContext.config.labels.review[reviewerGroup]) { const { data: reviews } = await context.github.pullRequests.listReviews( context.issue({ per_page: 50 }) ); @@ -121,7 +95,7 @@ module.exports = (app) => { } ); - if (config.labels.review[reviewerGroup]) { + if (repoContext.config.labels.review[reviewerGroup]) { const { data: reviews } = await context.github.pullRequests.listReviews( context.issue({ per_page: 50 }) ); @@ -186,7 +160,7 @@ module.exports = (app) => { const reviewerGroup = repoContext.getReviewerGroup(reviewer.login); - if (reviewerGroup && config.labels.review[reviewerGroup]) { + if (reviewerGroup && repoContext.config.labels.review[reviewerGroup]) { const hasRequestedReviewsForGroup = repoContext.reviewShouldWait( reviewerGroup, pr.requested_reviewers, @@ -251,7 +225,7 @@ module.exports = (app) => { const reviewerGroup = repoContext.getReviewerGroup(reviewer.login); - if (reviewerGroup && config.labels.review[reviewerGroup]) { + if (reviewerGroup && repoContext.config.labels.review[reviewerGroup]) { const { data: reviews } = await context.github.pullRequests.listReviews( context.issue({ per_page: 50 }) ); @@ -311,7 +285,7 @@ module.exports = (app) => { if (!repoContext) return; await Promise.all([ - editOpenedPr(repoContext, context), + editOpenedPR(repoContext, context), repoContext.addStatusCheckToLatestCommit(context), ]); }); @@ -320,6 +294,9 @@ module.exports = (app) => { const repoContext = await obtainRepoContext(context); if (!repoContext) return; - await editOpenedPr(repoContext, context); + await Promise.all([ + editOpenedPR(repoContext, context), + lintPR(repoContext, context), + ]); }); }; diff --git a/lib/teamconfigs/christophehurpeau.js b/lib/teamconfigs/christophehurpeau.js new file mode 100644 index 000000000..4c7834db1 --- /dev/null +++ b/lib/teamconfigs/christophehurpeau.js @@ -0,0 +1,67 @@ +'use strict'; + +module.exports = { + autoAssignToCreator: true, + trimTitle: true, + prLint: { + title: [ + { + regExp: + // eslint-disable-next-line unicorn/no-unsafe-regex + /^(revert: )?(build|chore|ci|docs|feat|fix|perf|refactor|style|test)(\(([a-z\-/]*)\))?:\s/, + error: { + title: 'Title does not match commitlint conventional', + summary: + 'https://github.com/marionebl/commitlint/tree/master/%40commitlint/config-conventional', + }, + }, + ], + }, + dev: { + christophehurpeau: 'christophe@hurpeau.com', + }, + waitForGroups: { + dev: [], + design: ['devs'], + }, + labels: { + list: { + // /* ci */ + // 'ci/in-progress': { name: ':green_heart: ci/in-progress', color: '#0052cc' }, + // 'ci/fail': { name: ':green_heart: ci/fail', color: '#e11d21' }, + // 'ci/passed': { name: ':green_heart: ci/passed', color: '#86f9b4' }, + + /* code */ + 'code/needs-review': { + name: ':ok_hand: code/needs-review', + color: '#FFD57F', + }, + 'code/review-requested': { + name: ':ok_hand: code/review-requested', + color: '#B2E1FF', + }, + 'code/changes-requested': { + name: ':ok_hand: code/changes-requested', + color: '#e11d21', + }, + 'code/approved': { + name: ':ok_hand: code/approved', + color: '#64DD17', + }, + }, + + review: { + ci: { + inProgress: 'ci/in-progress', + succeeded: 'ci/success', + failed: 'ci/fail', + }, + dev: { + needsReview: 'code/needs-review', + requested: 'code/review-requested', + changesRequested: 'code/changes-requested', + approved: 'code/approved', + }, + }, + }, +}; diff --git a/lib/teamconfigs/index.js b/lib/teamconfigs/index.js new file mode 100644 index 000000000..576c93e65 --- /dev/null +++ b/lib/teamconfigs/index.js @@ -0,0 +1,4 @@ +'use strict'; + +exports.ornikar = require('./ornikar'); +exports.christophehurpeau = require('./christophehurpeau'); diff --git a/lib/teamconfig.js b/lib/teamconfigs/ornikar.js similarity index 80% rename from lib/teamconfig.js rename to lib/teamconfigs/ornikar.js index 7a3b8d87e..0509570a8 100644 --- a/lib/teamconfig.js +++ b/lib/teamconfigs/ornikar.js @@ -4,6 +4,27 @@ module.exports = { slackToken: process.env.ORNIKAR_SLACK_TOKEN, autoAssignToCreator: true, trimTitle: true, + prLint: { + title: [ + { + regExp: + // eslint-disable-next-line unicorn/no-unsafe-regex + /^(revert: )?(build|chore|ci|docs|feat|fix|perf|refactor|style|test)(\(([a-z\-/]*)\))?:\s/, + error: { + title: 'Title does not match commitlint conventional', + summary: + 'https://github.com/marionebl/commitlint/tree/master/%40commitlint/config-conventional', + }, + }, + { + regExp: /\s(ONK-(\d+)|\[no issue])$/, + error: { + title: 'Title does not have JIRA issue', + summary: 'The PR title should end with ONK-0000, or [no issue]', + }, + }, + ], + }, dev: { abarreir: `alexandre${process.env.ORNIKAR_EMAIL_DOMAIN}`, christophehurpeau: `christophe${process.env.ORNIKAR_EMAIL_DOMAIN}`, diff --git a/package.json b/package.json index b62d4c03a..94919ca73 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "repository": "git@github.com:christophehurpeau/reviewflow.git", "homepage": "https://github.com/christophehurpeau/reviewflow", "engines": { - "node": ">=10.6.0 < 11.0.0" + "node": ">=10.6.0" }, "main": "./lib/index.js", "scripts": { @@ -71,7 +71,7 @@ "jest": "^23.6.0", "lint-staged": "8.1.0", "localtunnel": "1.9.1", - "nodemon": "1.18.6", + "nodemon": "1.18.9", "pob-release": "4.6.1", "prettier": "1.15.2", "yarnhook": "0.3.0" diff --git a/yarn.lock b/yarn.lock index a6131289f..44458ba65 100644 --- a/yarn.lock +++ b/yarn.lock @@ -374,17 +374,7 @@ agent-base@4, agent-base@^4.1.0: dependencies: es6-promisify "^5.0.0" -ajv@^6.5.3: - version "6.5.5" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.5.5.tgz#cf97cdade71c6399a92c6d6c4177381291b781a1" - integrity sha512-7q7gtRQDJSyuEHjuVgHoUa2VuemFiCMrfQc9Tc08XTAc4Zj/5U1buQJ0HU6i7fKjXU09SVgSmxa4sLvuvS8Iyg== - dependencies: - fast-deep-equal "^2.0.1" - fast-json-stable-stringify "^2.0.0" - json-schema-traverse "^0.4.1" - uri-js "^4.2.2" - -ajv@^6.5.5: +ajv@^6.5.3, ajv@^6.5.5: version "6.6.1" resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.6.1.tgz#6360f5ed0d80f232cc2b294c362d5dc2e538dd61" integrity sha512-ZoJjft5B+EJBjUyu9C9Hc0OZyPZSSlOF+plzouTrg6UlA8f+e/n8NIgBFG/9tppJtpPWfthHakK7juJdNDODww== @@ -4876,21 +4866,21 @@ node-pre-gyp@^0.10.0: semver "^5.3.0" tar "^4" -nodemon@1.18.6: - version "1.18.6" - resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-1.18.6.tgz#89b1136634d6c0afc7de24cc932a760e999e2c76" - integrity sha512-4pHQNYEZun+IkIC2jCaXEhkZnfA7rQe73i8RkdRyDJls/K+WxR7IpI5uNUsAvQ0zWvYcCDNGD+XVtw2ZG86/uQ== +nodemon@1.18.9: + version "1.18.9" + resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-1.18.9.tgz#90b467efd3b3c81b9453380aeb2a2cba535d0ead" + integrity sha512-oj/eEVTEI47pzYAjGkpcNw0xYwTl4XSTUQv2NPQI6PpN3b75PhpuYk3Vb3U80xHCyM2Jm+1j68ULHXl4OR3Afw== dependencies: chokidar "^2.0.4" debug "^3.1.0" ignore-by-default "^1.0.1" minimatch "^3.0.4" - pstree.remy "^1.1.0" + pstree.remy "^1.1.6" semver "^5.5.0" supports-color "^5.2.0" touch "^3.1.0" undefsafe "^2.0.2" - update-notifier "^2.3.0" + update-notifier "^2.5.0" nopt@^4.0.1: version "4.0.1" @@ -5549,10 +5539,10 @@ psl@^1.1.24, psl@^1.1.28: resolved "https://registry.yarnpkg.com/psl/-/psl-1.1.29.tgz#60f580d360170bb722a797cc704411e6da850c67" integrity sha512-AeUmQ0oLN02flVHXWh9sSJF7mcdFq0ppid/JkErufc3hGIV/AMa8Fo9VgDo/cT2jFdOWoFvHp90qqBH54W+gjQ== -pstree.remy@^1.1.0: - version "1.1.2" - resolved "https://registry.yarnpkg.com/pstree.remy/-/pstree.remy-1.1.2.tgz#4448bbeb4b2af1fed242afc8dc7416a6f504951a" - integrity sha512-vL6NLxNHzkNTjGJUpMm5PLC+94/0tTlC1vkP9bdU0pOHih+EujMjgMTwfZopZvHWRFbqJ5Y73OMoau50PewDDA== +pstree.remy@^1.1.6: + version "1.1.6" + resolved "https://registry.yarnpkg.com/pstree.remy/-/pstree.remy-1.1.6.tgz#73a55aad9e2d95814927131fbf4dc1b62d259f47" + integrity sha512-NdF35+QsqD7EgNEI5mkI/X+UwaxVEbQaz9f4IooEmMUv6ZPmlTQYGjBPJGgrlzNdjSvIy4MWMg6Q6vCgBO2K+w== pump@^3.0.0: version "3.0.0" @@ -6776,7 +6766,7 @@ update-dotenv@^1.1.0: resolved "https://registry.yarnpkg.com/update-dotenv/-/update-dotenv-1.1.1.tgz#17146f302f216c3c92419d5a327a45be910050ca" integrity sha512-3cIC18In/t0X/yH793c00qqxcKD8jVCgNOPif/fGQkFpYMGecM9YAc+kaAKXuZsM2dE9I9wFI7KvAuNX22SGMQ== -update-notifier@^2.3.0: +update-notifier@^2.5.0: version "2.5.0" resolved "https://registry.yarnpkg.com/update-notifier/-/update-notifier-2.5.0.tgz#d0744593e13f161e406acb1d9408b72cad08aff6" integrity sha512-gwMdhgJHGuj/+wHJJs9e6PcCszpxR1b236igrOkUofGhqJuG+amlIKwApH1IW1WWl7ovZxsX49lMBWLxSdm5Dw==