Skip to content

Commit

Permalink
Add files via upload
Browse files Browse the repository at this point in the history
  • Loading branch information
kartik1000 committed Jun 22, 2020
1 parent 41f30e8 commit 01b99b3
Show file tree
Hide file tree
Showing 3 changed files with 365 additions and 0 deletions.
72 changes: 72 additions & 0 deletions lib/schema.js
@@ -0,0 +1,72 @@
const Joi = require('@hapi/joi')

const fields = {
daysUntilStale: Joi.number()
.description('Number of days of inactivity before an Issue or Pull Request becomes stale'),

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.'),

onlyLabels: Joi.alternatives().try(Joi.any().valid(null), Joi.array().single())
.description('Only issues or pull requests with all of these labels are checked for staleness. Set to `[]` to disable'),

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)'),

exemptAssignees: Joi.boolean()
.description('Set to true to ignore issues with an assignee (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),
daysUntilClose: fields.daysUntilClose.default(7),
onlyLabels: fields.onlyLabels.default([]),
exemptLabels: fields.exemptLabels.default(['pinned', 'security']),
exemptProjects: fields.exemptProjects.default(false),
exemptMilestones: fields.exemptMilestones.default(false),
exemptAssignees: fields.exemptMilestones.default(false),
staleLabel: fields.staleLabel.default('wontfix'),
markComment: fields.markComment.default(
'Is this still relevant? If so, what is blocking it? ' +
'Is there anything you can do to help move it forward?' +
'\n\nThis issue has been automatically marked as stale ' +
'because it has not had recent activity. ' +
'It will be closed if no further activity occurs.'
),
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
66 changes: 66 additions & 0 deletions lib/stale-plugin.js
@@ -0,0 +1,66 @@
require('newrelic')

const getConfig = require('probot-config')
const createScheduler = require('probot-scheduler')
const Stale = require('./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, unmark)
app.on('schedule.repository', markAndSweep)

async function unmark (context) {
if (!context.isBot) {
const stale = await forRepository(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.unmarkIssue(type, issue)
}
}
}

async function markAndSweep (context) {
const stale = await forRepository(context)
await stale.markAndSweep('pulls')
await stale.markAndSweep('issues')
}

async function forRepository (context) {
let config = await getConfig(context, 'stale.yml')

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)
}
}
227 changes: 227 additions & 0 deletions lib/stale.js
@@ -0,0 +1,227 @@
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.mark(type)
await this.sweep(type)
}

async mark (type) {
await this.ensureStaleLabelExists(type)

const staleItems = (await this.getStale(type)).data.items

await Promise.all(
staleItems
.filter(issue => !issue.locked && issue.state !== 'closed')
.map(issue => this.markIssue(type, issue))
)
}

async sweep (type) {
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 && issue.state !== 'closed')
.map(issue => this.close(type, issue))
)
} else {
this.logger.trace({ owner, repo }, 'Configured to leave stale issues open')
}
}

getStale (type) {
const onlyLabels = this.getConfigValue(type, 'onlyLabels')
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 exemptAssignees = this.getConfigValue(type, 'exemptAssignees')
const labels = [staleLabel].concat(exemptLabels)
const queryParts = labels.map(label => `-label:"${label}"`)
queryParts.push(...onlyLabels.map(label => `label:"${label}"`))
queryParts.push(Stale.getQueryTypeRestriction(type))

queryParts.push(exemptProjects ? 'no:project' : '')
queryParts.push(exemptMilestones ? 'no:milestone' : '')
queryParts.push(exemptAssignees ? 'no:assignee' : '')

const query = queryParts.join(' ')
const days = this.getConfigValue(type, '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 markIssue (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.edit({ owner, repo, number, state: 'closed' })
} else {
this.logger.info('%s/%s#%d would have been closed (dry-run)', owner, repo, number)
}
}

async unmarkIssue (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
}
}

1 comment on commit 01b99b3

@vercel
Copy link

@vercel vercel bot commented on 01b99b3 Jun 22, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deployment has failed due to an internal error. (code: undefined)

Contact our support with support@vercel.com for more information.

Please sign in to comment.