From ecb0e912314e17d95607fa7754c889d4c8d02933 Mon Sep 17 00:00:00 2001 From: dessant Date: Sun, 6 May 2018 09:04:43 +0300 Subject: [PATCH] feat: validate config and set custom options for issues and pulls --- README.md | 28 +++++++++++++++------- assets/app-description.md | 28 +++++++++++++++------- package.json | 1 + src/defaults.js | 9 ------- src/index.js | 30 ++++++++++++++++++++++-- src/lock.js | 49 ++++++++++++++++++++++----------------- src/schema.js | 44 +++++++++++++++++++++++++++++++++++ yarn.lock | 28 ++++++++++++++++++++++ 8 files changed, 169 insertions(+), 48 deletions(-) delete mode 100644 src/defaults.js create mode 100644 src/schema.js diff --git a/README.md b/README.md index 69ea611..a4d3e09 100644 --- a/README.md +++ b/README.md @@ -30,18 +30,30 @@ The file can be empty, or it can override any of these default settings: # Number of days of inactivity before a closed issue or pull request is locked daysUntilLock: 365 + +# Issues and pull requests with these labels will not be locked. Set to `[]` to disable +exemptLabels: [] + +# Label to add before locking, such as `outdated`. Set to `false` to disable +lockLabel: false + # Comment to post before locking. Set to `false` to disable lockComment: > - This thread has been automatically locked because it has not had recent - activity. Please open a new issue for related bugs and link to relevant - comments in this thread. -# Issues or pull requests with these labels will not be locked -# exemptLabels: -# - no-locking + This thread has been automatically locked since there has not been + any recent activity after it was closed. Please open a new issue for + related bugs. + # Limit to only `issues` or `pulls` # only: issues -# Add a label when locking. Set to `false` to disable -# lockLabel: outdated + +# Optionally, specify configuration settings just for `issues` or `pulls` +# issues: +# exemptLabels: +# - help-wanted +# lockLabel: outdated + +# pulls: +# daysUntilLock: 30 ``` ## How are issues and pull requests determined to be inactive? diff --git a/assets/app-description.md b/assets/app-description.md index c6cdb4f..60aac7d 100644 --- a/assets/app-description.md +++ b/assets/app-description.md @@ -17,18 +17,30 @@ Create `.github/lock.yml` in the default branch to enable the app. The file can # Number of days of inactivity before a closed issue or pull request is locked daysUntilLock: 365 + +# Issues and pull requests with these labels will not be locked. Set to `[]` to disable +exemptLabels: [] + +# Label to add before locking, such as `outdated`. Set to `false` to disable +lockLabel: false + # Comment to post before locking. Set to `false` to disable lockComment: > - This thread has been automatically locked because it has not had recent - activity. Please open a new issue for related bugs and link to relevant - comments in this thread. -# Issues or pull requests with these labels will not be locked -# exemptLabels: -# - no-locking + This thread has been automatically locked since there has not been + any recent activity after it was closed. Please open a new issue for + related bugs. + # Limit to only `issues` or `pulls` # only: issues -# Add a label when locking. Set to `false` to disable -# lockLabel: outdated + +# Optionally, specify configuration settings just for `issues` or `pulls` +# issues: +# exemptLabels: +# - help-wanted +# lockLabel: outdated + +# pulls: +# daysUntilLock: 30 ``` ## Supporting the Project diff --git a/package.json b/package.json index b29b079..77bf91e 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "release": "standard-version && yarn run push" }, "dependencies": { + "joi": "^13.2.0", "probot": "^6.1.0", "probot-scheduler": "^1.1.0" }, diff --git a/src/defaults.js b/src/defaults.js deleted file mode 100644 index c48e731..0000000 --- a/src/defaults.js +++ /dev/null @@ -1,9 +0,0 @@ -module.exports = { - daysUntilLock: 365, - exemptLabels: [], - lockLabel: false, - lockComment: - 'This thread has been automatically locked because ' + - 'it has not had recent activity. Please open a new issue ' + - 'for related bugs and link to relevant comments in this thread.' -}; diff --git a/src/index.js b/src/index.js index 2e70032..112c5c2 100644 --- a/src/index.js +++ b/src/index.js @@ -1,6 +1,7 @@ const createScheduler = require('probot-scheduler'); const App = require('./lock'); +const schema = require('./schema'); module.exports = robot => { scheduler = createScheduler(robot); @@ -8,14 +9,39 @@ module.exports = robot => { robot.on('schedule.repository', async context => { const app = await getApp(context); if (app) { - await app.lock(); + const {only: type} = app.config; + if (type) { + await app.lock(type); + } else { + await app.lock('issues'); + await app.lock('pulls'); + } } }); async function getApp(context) { - let config = await context.config('lock.yml'); + const config = await getConfig(context); if (config) { return new App(context, config, robot.log); } } + + async function getConfig(context) { + const {owner, repo} = context.issue(); + let config; + try { + const repoConfig = await context.config('lock.yml'); + if (repoConfig) { + const {error, value} = schema.validate(repoConfig); + if (error) { + throw error; + } + config = value; + } + } catch (err) { + robot.log.warn({err: new Error(err), owner, repo}, 'Invalid config'); + } + + return config; + } }; diff --git a/src/lock.js b/src/lock.js index b5e458d..a8538e4 100644 --- a/src/lock.js +++ b/src/lock.js @@ -1,17 +1,16 @@ -const defaults = require('./defaults'); - module.exports = class Lock { constructor(context, config, logger) { this.context = context; - this.config = Object.assign({}, defaults, config); + this.config = config; this.logger = logger; } - async lock() { + async lock(type) { const {owner, repo} = this.context.repo(); - const {lockComment, lockLabel} = this.config; + const lockLabel = this.getConfigValue(type, 'lockLabel'); + const lockComment = this.getConfigValue(type, 'lockComment'); - const issues = await this.getLockableIssues(); + const issues = await this.getLockableIssues(type); for (const issue of issues) { const issueUrl = `${owner}/${repo}/issues/${issue.number}`; if (lockComment) { @@ -47,17 +46,12 @@ module.exports = class Lock { } } - async getLockableIssues() { - const results = await this.search(); - return results.data.items.filter(issue => !issue.locked); - } - - search() { + search(type) { const {owner, repo} = this.context.repo(); - const {exemptLabels, daysUntilLock, only} = this.config; - const timestamp = this.since(daysUntilLock) - .toISOString() - .replace(/\.\d{3}\w$/, ''); + const daysUntilLock = this.getConfigValue(type, 'daysUntilLock'); + const exemptLabels = this.getConfigValue(type, 'exemptLabels'); + + const timestamp = this.getUpdatedTimestamp(daysUntilLock); let query = `repo:${owner}/${repo} is:closed updated:<${timestamp}`; if (exemptLabels.length) { @@ -66,13 +60,13 @@ module.exports = class Lock { .join(' '); query += ` ${queryPart}`; } - if (only === 'issues') { + if (type === 'issues') { query += ' is:issue'; - } else if (only === 'pulls') { + } else { query += ' is:pr'; } - this.logger.info(`[${owner}/${repo}] Searching`); + this.logger.info(`[${owner}/${repo}] Searching ${type}`); return this.context.github.search.issues({ q: query, sort: 'updated', @@ -81,8 +75,21 @@ module.exports = class Lock { }); } - since(days) { + async getLockableIssues(type) { + const results = await this.search(type); + return results.data.items.filter(issue => !issue.locked); + } + + getUpdatedTimestamp(days) { const ttl = days * 24 * 60 * 60 * 1000; - return new Date(new Date() - ttl); + const date = new Date(new Date() - ttl); + return date.toISOString().replace(/\.\d{3}\w$/, ''); + } + + getConfigValue(type, key) { + if (this.config[type] && typeof this.config[type][key] !== 'undefined') { + return this.config[type][key]; + } + return this.config[key]; } }; diff --git a/src/schema.js b/src/schema.js new file mode 100644 index 0000000..7beb395 --- /dev/null +++ b/src/schema.js @@ -0,0 +1,44 @@ +const Joi = require('joi'); + +const fields = { + daysUntilLock: Joi.number() + .min(1) + .description( + 'Number of days of inactivity before a closed issue or pull request is locked' + ), + + exemptLabels: Joi.array() + .single() + .items(Joi.string()) + .description( + 'Issues and pull requests with these labels will not be locked. Set to `[]` to disable' + ), + + lockLabel: Joi.alternatives() + .try(Joi.string(), Joi.boolean().only(false)) + .description( + 'Label to add before locking, such as `outdated`. Set to `false` to disable' + ), + + lockComment: Joi.alternatives() + .try(Joi.string(), Joi.boolean().only(false)) + .description('Comment to post before locking. Set to `false` to disable') +}; + +const schema = Joi.object().keys({ + daysUntilLock: fields.daysUntilLock.default(365), + exemptLabels: fields.exemptLabels.default([]), + lockLabel: fields.lockLabel.default(false), + lockComment: fields.lockComment.default( + 'This thread has been automatically locked since there has not been ' + + 'any recent activity after it was closed. Please open a new issue for ' + + 'related bugs.' + ), + only: Joi.string() + .valid('issues', 'pulls') + .description('Limit to only `issues` or `pulls`'), + pulls: Joi.object().keys(fields), + issues: Joi.object().keys(fields) +}); + +module.exports = schema; diff --git a/yarn.lock b/yarn.lock index 39a754c..831d6d7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1664,6 +1664,10 @@ hoek@4.x.x: version "4.2.0" resolved "https://registry.yarnpkg.com/hoek/-/hoek-4.2.0.tgz#72d9d0754f7fe25ca2d01ad8f8f9a9449a89526d" +hoek@5.x.x: + version "5.0.3" + resolved "https://registry.yarnpkg.com/hoek/-/hoek-5.0.3.tgz#b71d40d943d0a95da01956b547f83c4a5b4a34ac" + hosted-git-info@^2.1.4, hosted-git-info@^2.1.5, hosted-git-info@^2.4.2: version "2.5.0" resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.5.0.tgz#6d60e34b3abbc8313062c3b798ef8d901a07af3c" @@ -1975,6 +1979,12 @@ isarray@1.0.0, isarray@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" +isemail@3.x.x: + version "3.1.2" + resolved "https://registry.yarnpkg.com/isemail/-/isemail-3.1.2.tgz#937cf919002077999a73ea8b1951d590e84e01dd" + dependencies: + punycode "2.x.x" + isexe@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" @@ -1997,6 +2007,14 @@ jju@^1.1.0: version "1.3.0" resolved "https://registry.yarnpkg.com/jju/-/jju-1.3.0.tgz#dadd9ef01924bc728b03f2f7979bdbd62f7a2aaa" +joi@^13.2.0: + version "13.2.0" + resolved "https://registry.yarnpkg.com/joi/-/joi-13.2.0.tgz#72307f1765bb40b068361f9368a4ba1092b8478e" + dependencies: + hoek "5.x.x" + isemail "3.x.x" + topo "3.x.x" + js-yaml@^3.6.1: version "3.11.0" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.11.0.tgz#597c1a8bd57152f26d622ce4117851a51f5ebaef" @@ -3128,6 +3146,10 @@ pstree.remy@^1.1.0: dependencies: ps-tree "^1.1.0" +punycode@2.x.x: + version "2.1.0" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.0.tgz#5f863edc89b96db09074bad7947bf09056ca4e7d" + punycode@^1.4.1: version "1.4.1" resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" @@ -3977,6 +3999,12 @@ to-regex@^3.0.1, to-regex@^3.0.2: regex-not "^1.0.2" safe-regex "^1.1.0" +topo@3.x.x: + version "3.0.0" + resolved "https://registry.yarnpkg.com/topo/-/topo-3.0.0.tgz#37e48c330efeac784538e0acd3e62ca5e231fe7a" + dependencies: + hoek "5.x.x" + touch@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/touch/-/touch-3.1.0.tgz#fe365f5f75ec9ed4e56825e0bb76d24ab74af83b"