diff --git a/bot_scripts/stale/index.js b/bot_scripts/stale/index.js deleted file mode 100644 index d738a53..0000000 --- a/bot_scripts/stale/index.js +++ /dev/null @@ -1,81 +0,0 @@ -// Description: -// A GitHub App built with Probot that closes abandoned Issues and Pull Requests after a period of inactivity. https://probot.github.io/apps/stale/ -// -// Dependencies: -// github: "^13.1.0" -// joi: "^13.1.2" -// probot-config: "^1.0.0" -// probot-scheduler: "^1.2.0" -// -// Author: -// https://probot.github.io/apps/stale/ - -const getConfig = require('probot-config') -const createScheduler = require('probot-scheduler') -const defaultConfig = require('../../lib/config') -const Stale = require('./lib/stale') - -module.exports = async app => { - // Visit all repositories to mark and sweep stale issues - const scheduler = createScheduler(app) - - // Unmark stale issues if a user comments - const events = [ - 'issue_comment', - 'issues', - 'pull_request', - 'pull_request_review', - 'pull_request_review_comment' - ] - - app.on(events, context => unmark(app, context)) - app.on('schedule.repository', context => markAndSweep(app, context)) - - async function unmark (robot, context) { - if (!context.isBot) { - const stale = await forRepository(robot, context) - let issue = context.payload.issue || context.payload.pull_request - const type = context.payload.issue ? 'issues' : 'pulls' - - // Some payloads don't include labels - if (!issue.labels) { - try { - issue = (await context.github.issues.get(context.issue())).data - } catch (error) { - context.log('Issue not found') - } - } - - const staleLabelAdded = context.payload.action === 'labeled' && - context.payload.label.name === stale.config.staleLabel - - if (stale.hasStaleLabel(type, issue) && issue.state !== 'closed' && !staleLabelAdded) { - stale.unmark(type, issue) - } - } - } - - async function markAndSweep (robot, context) { - const stale = await forRepository(robot, context) - await stale.markAndSweep('pulls') - await stale.markAndSweep('issues') - } - - async function forRepository (robot, context) { - let config = await getConfig(context, 'github-bot.yml', defaultConfig(robot, '.github/github-bot.yml')) - - if (config) { - config = config.stale - } - - if (!config) { - scheduler.stop(context.payload.repository) - // Don't actually perform for repository without a config - config = { perform: false } - } - - config = Object.assign(config, context.repo({ logger: app.log })) - - return new Stale(context.github, config) - } -} diff --git a/bot_scripts/stale/lib/schema.js b/bot_scripts/stale/lib/schema.js deleted file mode 100644 index 63ddf02..0000000 --- a/bot_scripts/stale/lib/schema.js +++ /dev/null @@ -1,66 +0,0 @@ -const Joi = require('joi') - -const fields = { - daysUntilStale: Joi.number() - .description('Number of days of inactivity before an Issue or Pull Request becomes stale'), - - daysUntilPullRequestStale: Joi.number() - .description('Number of days of inactivity before a Pull Request becomes stale (overrides daysUntilStale for PRs)'), - - daysUntilClose: Joi.alternatives().try(Joi.number(), Joi.boolean().only(false)) - .error(() => '"daysUntilClose" must be a number or false') - .description('Number of days of inactivity before a stale Issue or Pull Request is closed. If disabled, issues still need to be closed manually, but will remain marked as stale.'), - - exemptLabels: Joi.alternatives().try(Joi.any().valid(null), Joi.array().single()) - .description('Issues or Pull Requests with these labels will never be considered stale. Set to `[]` to disable'), - - exemptProjects: Joi.boolean() - .description('Set to true to ignore issues in a project (defaults to false)'), - - exemptMilestones: Joi.boolean() - .description('Set to true to ignore issues in a milestone (defaults to false)'), - - staleLabel: Joi.string() - .description('Label to use when marking as stale'), - - markComment: Joi.alternatives().try(Joi.string(), Joi.any().only(false)) - .error(() => '"markComment" must be a string or false') - .description('Comment to post when marking as stale. Set to `false` to disable'), - - unmarkComment: Joi.alternatives().try(Joi.string(), Joi.boolean().only(false)) - .error(() => '"unmarkComment" must be a string or false') - .description('Comment to post when removing the stale label. Set to `false` to disable'), - - closeComment: Joi.alternatives().try(Joi.string(), Joi.boolean().only(false)) - .error(() => '"closeComment" must be a string or false') - .description('Comment to post when closing a stale Issue or Pull Request. Set to `false` to disable'), - - limitPerRun: Joi.number().integer().min(1).max(30) - .error(() => '"limitPerRun" must be an integer between 1 and 30') - .description('Limit the number of actions per hour, from 1-30. Default is 30') -} - -const schema = Joi.object().keys({ - daysUntilStale: fields.daysUntilStale.default(60), - daysUntilPullRequestStale: fields.daysUntilPullRequestStale, - daysUntilClose: fields.daysUntilClose.default(7), - exemptLabels: fields.exemptLabels.default(['pinned', 'security']), - exemptProjects: fields.exemptProjects.default(false), - exemptMilestones: fields.exemptMilestones.default(false), - staleLabel: fields.staleLabel.default('wontfix'), - markComment: fields.markComment.default( - 'This issue has been automatically marked as stale because ' + - 'it has not had recent activity. It will be closed if no further ' + - 'activity occurs. Thank you for your contributions.' - ), - unmarkComment: fields.unmarkComment.default(false), - closeComment: fields.closeComment.default(false), - limitPerRun: fields.limitPerRun.default(30), - perform: Joi.boolean().default(!process.env.DRY_RUN), - only: Joi.any().valid('issues', 'pulls', null).description('Limit to only `issues` or `pulls`'), - pulls: Joi.object().keys(fields), - issues: Joi.object().keys(fields), - _extends: Joi.string().description('Repository to extend settings from') -}) - -module.exports = schema diff --git a/bot_scripts/stale/lib/stale.js b/bot_scripts/stale/lib/stale.js deleted file mode 100644 index 7e5fe67..0000000 --- a/bot_scripts/stale/lib/stale.js +++ /dev/null @@ -1,218 +0,0 @@ -const schema = require('./schema') -const maxActionsPerRun = 30 - -module.exports = class Stale { - constructor (github, { owner, repo, logger = console, ...config }) { - this.github = github - this.logger = logger - this.remainingActions = 0 - - const { error, value } = schema.validate(config) - - this.config = value - if (error) { - // Report errors to sentry - logger.warn({ err: new Error(error), owner, repo }, 'Invalid config') - } - - Object.assign(this.config, { owner, repo }) - } - - async markAndSweep (type) { - const { only } = this.config - if (only && only !== type) { - return - } - if (!this.getConfigValue(type, 'perform')) { - return - } - - this.logger.info(this.config, `starting mark and sweep of ${type}`) - - const limitPerRun = this.getConfigValue(type, 'limitPerRun') || maxActionsPerRun - this.remainingActions = Math.min(limitPerRun, maxActionsPerRun) - - await this.ensureStaleLabelExists(type) - - const staleItems = (await this.getStale(type)).data.items - - await Promise.all(staleItems.filter(issue => !issue.locked).map(issue => { - return this.mark(type, issue) - })) - - const { owner, repo } = this.config - const daysUntilClose = this.getConfigValue(type, 'daysUntilClose') - - if (daysUntilClose) { - this.logger.trace({ owner, repo }, 'Configured to close stale issues') - const closableItems = (await this.getClosable(type)).data.items - - await Promise.all(closableItems.filter(issue => !issue.locked).map(issue => { - this.close(type, issue) - })) - } else { - this.logger.trace({ owner, repo }, 'Configured to leave stale issues open') - } - } - - getStale (type) { - const staleLabel = this.getConfigValue(type, 'staleLabel') - const exemptLabels = this.getConfigValue(type, 'exemptLabels') - const exemptProjects = this.getConfigValue(type, 'exemptProjects') - const exemptMilestones = this.getConfigValue(type, 'exemptMilestones') - const labels = [staleLabel].concat(exemptLabels) - const queryParts = labels.map(label => `-label:"${label}"`) - queryParts.push(Stale.getQueryTypeRestriction(type)) - - queryParts.push(exemptProjects ? 'no:project' : '') - queryParts.push(exemptMilestones ? 'no:milestone' : '') - - const query = queryParts.join(' ') - let days = this.getConfigValue(type, 'days') - if (!days && type === 'pulls') { - days = this.getConfigValue(type, 'daysUntilPullRequestStale') - } - if (!days) { - days = this.getConfigValue(type, 'daysUntilStale') - } - return this.search(type, days, query) - } - - getClosable (type) { - const staleLabel = this.getConfigValue(type, 'staleLabel') - const queryTypeRestriction = Stale.getQueryTypeRestriction(type) - const query = `label:"${staleLabel}" ${queryTypeRestriction}` - const days = this.getConfigValue(type, 'days') || this.getConfigValue(type, 'daysUntilClose') - return this.search(type, days, query) - } - - static getQueryTypeRestriction (type) { - if (type === 'pulls') { - return 'is:pr' - } else if (type === 'issues') { - return 'is:issue' - } - throw new Error(`Unknown type: ${type}. Valid types are 'pulls' and 'issues'`) - } - - search (type, days, query) { - const { owner, repo } = this.config - const timestamp = this.since(days).toISOString().replace(/\.\d{3}\w$/, '') - - query = `repo:${owner}/${repo} is:open updated:<${timestamp} ${query}` - - const params = { q: query, sort: 'updated', order: 'desc', per_page: maxActionsPerRun } - - this.logger.info(params, 'searching %s/%s for stale issues', owner, repo) - return this.github.search.issues(params) - } - - async mark (type, issue) { - if (this.remainingActions === 0) { - return - } - this.remainingActions-- - - const { owner, repo } = this.config - const perform = this.getConfigValue(type, 'perform') - const staleLabel = this.getConfigValue(type, 'staleLabel') - const markComment = this.getConfigValue(type, 'markComment') - const number = issue.number - - if (perform) { - this.logger.info('%s/%s#%d is being marked', owner, repo, number) - if (markComment) { - await this.github.issues.createComment({ owner, repo, number, body: markComment }) - } - return this.github.issues.addLabels({ owner, repo, number, labels: [staleLabel] }) - } else { - this.logger.info('%s/%s#%d would have been marked (dry-run)', owner, repo, number) - } - } - - async close (type, issue) { - if (this.remainingActions === 0) { - return - } - this.remainingActions-- - - const { owner, repo } = this.config - const perform = this.getConfigValue(type, 'perform') - const closeComment = this.getConfigValue(type, 'closeComment') - const number = issue.number - - if (perform) { - this.logger.info('%s/%s#%d is being closed', owner, repo, number) - if (closeComment) { - await this.github.issues.createComment({ owner, repo, number, body: closeComment }) - } - return this.github.issues.update({ owner, repo, number, state: 'closed' }) - } else { - this.logger.info('%s/%s#%d would have been closed (dry-run)', owner, repo, number) - } - } - - async unmark (type, issue) { - const { owner, repo } = this.config - const perform = this.getConfigValue(type, 'perform') - const staleLabel = this.getConfigValue(type, 'staleLabel') - const unmarkComment = this.getConfigValue(type, 'unmarkComment') - const number = issue.number - - if (perform) { - this.logger.info('%s/%s#%d is being unmarked', owner, repo, number) - - if (unmarkComment) { - await this.github.issues.createComment({ owner, repo, number, body: unmarkComment }) - } - - return this.github.issues.removeLabel({ owner, repo, number, name: staleLabel }).catch((err) => { - // ignore if it's a 404 because then the label was already removed - if (err.code !== 404) { - throw err - } - }) - } else { - this.logger.info('%s/%s#%d would have been unmarked (dry-run)', owner, repo, number) - } - } - - // Returns true if at least one exempt label is present. - hasExemptLabel (type, issue) { - const exemptLabels = this.getConfigValue(type, 'exemptLabels') - return issue.labels.some(label => exemptLabels.includes(label.name)) - } - - hasStaleLabel (type, issue) { - const staleLabel = this.getConfigValue(type, 'staleLabel') - return issue.labels.map(label => label.name).includes(staleLabel) - } - - // returns a type-specific config value if it exists, otherwise returns the top-level value. - getConfigValue (type, key) { - if (this.config[type] && typeof this.config[type][key] !== 'undefined') { - return this.config[type][key] - } - return this.config[key] - } - - async ensureStaleLabelExists (type) { - const { owner, repo } = this.config - const staleLabel = this.getConfigValue(type, 'staleLabel') - - return this.github.issues.getLabel({ owner, repo, name: staleLabel }).catch(() => { - return this.github.issues.createLabel({ owner, repo, name: staleLabel, color: 'ffffff' }) - }) - } - - since (days) { - const ttl = days * 24 * 60 * 60 * 1000 - let date = new Date(new Date() - ttl) - - // GitHub won't allow it - if (date < new Date(0)) { - date = new Date(0) - } - return date - } -} diff --git a/index.js b/index.js index 8d9c0cc..613cb6f 100644 --- a/index.js +++ b/index.js @@ -39,7 +39,6 @@ module.exports = async (robot) => { // require('./bot_scripts/tip-kudos-recipients')(robot) // require('./bot_scripts/check-bot-balance')(robot) require('./bot_scripts/manage-pr-checklist')(robot) - require('./bot_scripts/stale/index')(robot) // For more information on building apps: // https://probot.github.io/docs/ diff --git a/package.json b/package.json index c09223d..92990e0 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,6 @@ "hashmap": "^2.3.0", "hashset": "0.0.6", "jenkins": "^0.20.1", - "joi": "^13.7.0", "mem-cache": "0.0.5", "memjs": "^1.2.0", "probot": "^7.5.0", diff --git a/yarn.lock b/yarn.lock index 118b98f..beddd1a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2532,16 +2532,6 @@ hmac-drbg@^1.0.0: minimalistic-assert "^1.0.0" minimalistic-crypto-utils "^1.0.1" -hoek@5.x.x: - version "5.0.4" - resolved "https://registry.yarnpkg.com/hoek/-/hoek-5.0.4.tgz#0f7fa270a1cafeb364a4b2ddfaa33f864e4157da" - integrity sha512-Alr4ZQgoMlnere5FZJsIyfIjORBqZll5POhDsF4q64dPuJR6rNxXdDxtHSQq8OXRurhmx+PWYEE8bXRROY8h0w== - -hoek@6.x.x: - version "6.1.2" - resolved "https://registry.yarnpkg.com/hoek/-/hoek-6.1.2.tgz#99e6d070561839de74ee427b61aa476bd6bddfd6" - integrity sha512-6qhh/wahGYZHFSFw12tBbJw5fsAhhwrrG/y3Cs0YMTv2WzMnL0oLPnQJjv1QJvEfylRSOFuP+xCu+tdx0tD16Q== - home-or-tmp@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/home-or-tmp/-/home-or-tmp-2.0.0.tgz#e36c3f2d2cae7d746a857e38d18d5f32a7882db8" @@ -3017,13 +3007,6 @@ isarray@1.0.0, isarray@^1.0.0, isarray@~1.0.0: resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE= -isemail@3.x.x: - version "3.2.0" - resolved "https://registry.yarnpkg.com/isemail/-/isemail-3.2.0.tgz#59310a021931a9fb06bbb51e155ce0b3f236832c" - integrity sha512-zKqkK+O+dGqevc93KNsbZ/TqTUFd46MwWjYOoMrjIMZ51eU7DtQG3Wmd9SQQT7i7RVnuTPEiYEWHU3MSbxC1Tg== - 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" @@ -3407,15 +3390,6 @@ jest@^22.4.4: import-local "^1.0.0" jest-cli "^22.4.4" -joi@^13.7.0: - version "13.7.0" - resolved "https://registry.yarnpkg.com/joi/-/joi-13.7.0.tgz#cfd85ebfe67e8a1900432400b4d03bbd93fb879f" - integrity sha512-xuY5VkHfeOYK3Hdi91ulocfuFopwgbSORmIwzcwHKESQhC7w1kD5jaVSPnqDxS2I8t3RZ9omCKAxNwXN5zG1/Q== - dependencies: - hoek "5.x.x" - isemail "3.x.x" - topo "3.x.x" - js-sha3@0.5.7: version "0.5.7" resolved "https://registry.yarnpkg.com/js-sha3/-/js-sha3-0.5.7.tgz#0d4ffd8002d5333aabaf4a23eed2f6374c9f28e7" @@ -4703,16 +4677,16 @@ pump@^3.0.0: end-of-stream "^1.1.0" once "^1.3.1" -punycode@2.x.x, punycode@^2.1.0, punycode@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" - integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== - punycode@^1.4.1: version "1.4.1" resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" integrity sha1-wNWmOycYgArY4esPpSachN1BhF4= +punycode@^2.1.0, punycode@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" + integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== + qs@6.5.2, qs@~6.5.2: version "6.5.2" resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" @@ -5741,13 +5715,6 @@ 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.3" - resolved "https://registry.yarnpkg.com/topo/-/topo-3.0.3.tgz#d5a67fb2e69307ebeeb08402ec2a2a6f5f7ad95c" - integrity sha512-IgpPtvD4kjrJ7CRA3ov2FhWQADwv+Tdqbsf1ZnPUSAtCJ9e1Z44MmoSGDXGk4IppoZA7jd/QRkNddlLJWlUZsQ== - dependencies: - hoek "6.x.x" - tough-cookie@>=2.3.3: version "3.0.0" resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-3.0.0.tgz#d2bceddebde633153ff20a52fa844a0dc71dacef"