blob: 7aae71f77679a79dde04f3ebb14bd0bbf9f74a45 [file] [log] [blame]
import * as core from '@actions/core'
import * as fs from 'fs'
import * as github from '@actions/github'
import * as scramjet from 'scramjet'
import Config from './config'
import { GitHub } from '@actions/github/lib/utils'
/* eslint-disable import/no-unresolved */
import { IssueCommentEvent } from '@octokit/webhooks-types'
import { RequestInterface } from '@octokit/types'
/* eslint-enable */
const fsp = fs.promises
interface Issue {
issue_number: number
owner: string
repo: string
}
interface LabeledEvent {
created_at: number
event: string
label: {
name: string
}
}
interface RestIssue {
number: number
}
export default class NoResponse {
config: Config
octokit: InstanceType<typeof GitHub>
constructor(config: Config) {
this.config = config
this.octokit = github.getOctokit(this.config.token)
}
async sweep(): Promise<void> {
core.debug('Starting sweep')
await this.ensureLabelExists()
const issues = await this.getCloseableIssues()
for (const issue of issues) {
this.close({ issue_number: issue.number, ...this.config.repo })
}
}
async unmark(): Promise<void> {
core.debug('Starting unmark')
const { responseRequiredLabel } = this.config
const payload: IssueCommentEvent = await this.readPayload()
const owner = payload.repository.owner.login
const repo = payload.repository.name
const { number } = payload.issue
const comment = payload.comment
const issue = { owner, repo, issue_number: number }
const issueInfo = await this.octokit.rest.issues.get(issue)
const isMarked = await this.hasResponseRequiredLabel(issue)
if (isMarked && issueInfo.data.user?.login === comment.user.login) {
core.info(`${owner}/${repo}#${number} is being unmarked`)
await this.octokit.rest.issues.removeLabel({
owner,
repo,
issue_number: number,
name: responseRequiredLabel
})
if (
issueInfo.data.state === 'closed' &&
issueInfo.data.user.login !== issueInfo.data.closed_by?.login
) {
this.octokit.rest.issues.update({ owner, repo, issue_number: number, state: 'open' })
}
}
}
async close(issue: Issue): Promise<void> {
const { closeComment } = this.config
core.info(`${issue.owner}/${issue.repo}#${issue.issue_number} is being closed`)
if (closeComment) {
await this.octokit.rest.issues.createComment({ body: closeComment, ...issue })
}
await this.octokit.rest.issues.update({ state: 'closed', ...issue })
}
async ensureLabelExists(): Promise<void> {
try {
await this.octokit.rest.issues.getLabel({
name: this.config.responseRequiredLabel,
...this.config.repo
})
} catch (e) {
this.octokit.rest.issues.createLabel({
name: this.config.responseRequiredLabel,
color: this.config.responseRequiredColor,
...this.config.repo
})
}
}
async findLastLabeledEvent(issue: Issue): Promise<LabeledEvent | undefined> {
const { responseRequiredLabel } = this.config
const events: LabeledEvent[] = await this.octokit.paginate(
(await this.octokit.rest.issues.listEvents({
...issue,
per_page: 100
})) as unknown as RequestInterface<object>
)
return events
.reverse()
.find((event) => event.event === 'labeled' && event.label.name === responseRequiredLabel)
}
async getCloseableIssues(): Promise<RestIssue[]> {
const { owner, repo } = this.config.repo
const { daysUntilClose, responseRequiredLabel } = this.config
const q = `repo:${owner}/${repo} is:issue is:open label:"${responseRequiredLabel}"`
const labeledEarlierThan = this.since(daysUntilClose)
const issues = await this.octokit.rest.search.issuesAndPullRequests({
q,
sort: 'updated',
order: 'asc',
per_page: 30
})
core.debug(`Issues to check for closing:`)
core.debug(JSON.stringify(issues, null, 2))
const closableIssues = await scramjet
.fromArray(issues.data.items)
.filter(async (issue) => {
const event = await this.findLastLabeledEvent({
issue_number: issue.number,
...this.config.repo
})
if (!event) {
return false
}
core.debug(`Checking: ${JSON.stringify(issue, null, 2)}`)
core.debug(`Using: ${JSON.stringify(event, null, 2)}`)
const creationDate = new Date(event.created_at)
core.debug(
`${creationDate.toISOString()} < ${labeledEarlierThan.toISOString()} === ${
creationDate < labeledEarlierThan
}`
)
return creationDate < labeledEarlierThan
})
.toArray()
core.debug(`Closeable: ${JSON.stringify(closableIssues, null, 2)}`)
return closableIssues
}
async hasResponseRequiredLabel(issue: Issue): Promise<boolean> {
const labels = await this.octokit.rest.issues.listLabelsOnIssue({ ...issue })
return labels.data.map((label) => label.name).includes(this.config.responseRequiredLabel)
}
async readPayload(): Promise<IssueCommentEvent> {
if (!process.env.GITHUB_EVENT_PATH) {
throw new Error('GITHUB_EVENT_PATH is not defined')
}
const text = (await fsp.readFile(process.env.GITHUB_EVENT_PATH)).toString()
return JSON.parse(text)
}
since(days: number): Date {
const ttl = days * 24 * 60 * 60 * 1000
return new Date(new Date().getTime() - ttl)
}
}