blob: d9d012c51a67fda7ee7a09e721a3f0ea6763b549 [file] [log] [blame]
import * as github from '@actions/github'
import * as core from '@actions/core'
import * as rest from '@octokit/rest'
function getRequiredEnv(key: string): string {
const value = process.env[key]
if (value === undefined) {
const message = `${key} was not defined.`
throw new Error(message)
}
return value
}
function verboseOutput(name: string, value: string): void {
core.info(`Setting output: ${name}: ${value}`)
core.setOutput(name, value)
}
async function getPullRequest(
octokit: github.GitHub,
owner: string,
repo: string,
pullRequestNumber: number
): Promise<rest.PullsGetResponse> {
core.info(`Fetching pull request with number: ${pullRequestNumber}`)
const pullRequest = await octokit.pulls.get({
owner,
repo,
// eslint-disable-next-line @typescript-eslint/camelcase
pull_number: pullRequestNumber
})
return pullRequest.data
}
function getPullRequestLabels(pullRequest: rest.PullsGetResponse): string[] {
const labelNames = pullRequest
? pullRequest.labels.map(label => label.name)
: []
return labelNames
}
async function getReviews(
octokit: github.GitHub,
owner: string,
repo: string,
number: number,
getComitters: boolean
): Promise<[rest.PullsListReviewsResponseItem[], string[], string[]]> {
let reviews: rest.PullsListReviewsResponseItem[] = []
const options = octokit.pulls.listReviews.endpoint.merge({
owner,
repo,
// eslint-disable-next-line @typescript-eslint/camelcase
pull_number: number
})
await octokit.paginate(options).then(r => {
reviews = r
})
const reviewers = reviews ? reviews.map(review => review.user.login) : []
const reviewersAlreadyChecked: string[] = []
const committers: string[] = []
if (getComitters) {
core.info('Checking reviewers permissions:')
for (const reviewer of reviewers) {
if (!reviewersAlreadyChecked.includes(reviewer)) {
const p = await octokit.repos.getCollaboratorPermissionLevel({
owner,
repo,
username: reviewer
})
const permission = p.data.permission
if (permission === 'admin' || permission === 'write') {
committers.push(reviewer)
}
core.info(`\t${reviewer}: ${permission}`)
reviewersAlreadyChecked.push(reviewer)
}
}
}
return [reviews, reviewers, committers]
}
function processReviews(
reviews: rest.PullsListReviewsResponseItem[],
reviewers: string[],
committers: string[],
requireCommittersApproval: boolean
): boolean {
let isApproved = false
const reviewStates: {[user: string]: string} = {}
for (const review of reviews) {
if (review.state === 'APPROVED' || review.state === 'CHANGES_REQUESTED') {
if (requireCommittersApproval && committers.includes(review.user.login)) {
reviewStates[review.user.login] = review.state
} else if (!requireCommittersApproval) {
reviewStates[review.user.login] = review.state
}
}
}
core.info(`Reviews:`)
for (const user in reviewStates) {
core.info(`\t${user}: ${reviewStates[user].toLowerCase()}`)
}
for (const user in reviewStates) {
if (reviewStates[user] === 'APPROVED') {
isApproved = true
break
}
}
for (const user in reviewStates) {
if (reviewStates[user] === 'CHANGES_REQUESTED') {
isApproved = false
break
}
}
return isApproved
}
async function setLabel(
octokit: github.GitHub,
owner: string,
repo: string,
pullRequestNumber: number,
label: string
): Promise<void> {
core.info(`Setting label: ${label}`)
await octokit.issues.addLabels({
// eslint-disable-next-line @typescript-eslint/camelcase
issue_number: pullRequestNumber,
labels: [label],
owner,
repo
})
}
async function removeLabel(
octokit: github.GitHub,
owner: string,
repo: string,
pullRequestNumber: number,
label: string
): Promise<void> {
core.info(`Removing label: ${label}`)
await octokit.issues.removeLabel({
// eslint-disable-next-line @typescript-eslint/camelcase
issue_number: pullRequestNumber,
name: label,
owner,
repo
})
}
async function addComment(
octokit: github.GitHub,
owner: string,
repo: string,
pullRequestNumber: number,
comment: string
): Promise<void> {
core.info(`Adding comment: ${comment}`)
await octokit.issues.createComment({
owner,
repo,
// eslint-disable-next-line @typescript-eslint/camelcase
issue_number: pullRequestNumber,
body: comment
})
}
async function printDebug(
item: object | string | boolean | number,
description: string = ''
): Promise<void> {
const itemJson = JSON.stringify(item)
core.debug(`\n ######### ${description} ######### \n: ${itemJson}\n\n`)
}
async function run(): Promise<void> {
const token = core.getInput('token', {required: true})
const userLabel = core.getInput('label') || 'not set'
const requireCommittersApproval =
core.getInput('require_committers_approval') === 'true'
const comment = core.getInput('comment') || ''
const pullRequestNumberInput = core.getInput('pullRequestNumber') || 'not set'
const octokit = new github.GitHub(token)
const context = github.context
const repository = getRequiredEnv('GITHUB_REPOSITORY')
const eventName = getRequiredEnv('GITHUB_EVENT_NAME')
const [owner, repo] = repository.split('/')
let pullRequestNumber: number | undefined
core.info(
`\n############### Set Label When Approved start ##################\n` +
`label: "${userLabel}"\n` +
`requireCommittersApproval: ${requireCommittersApproval}\n` +
`comment: ${comment}\n` +
`pullRequestNumber: ${pullRequestNumberInput}`
)
if (eventName === 'pull_request_review') {
pullRequestNumber = context.payload.pull_request
? context.payload.pull_request.number
: undefined
if (pullRequestNumber === undefined) {
throw Error(`Could not find PR number in context payload.`)
}
} else if (eventName === 'workflow_run') {
if (pullRequestNumberInput === 'not set') {
core.warning(
`If action is triggered by "workflow_run" then input "pullRequestNumber" is required.\n` +
`It might be missing because the pull request might have been already merged or a fixup pushed to` +
`the PR branch. None of the outputs will be set as we cannot find the right PR.`
)
return
} else {
pullRequestNumber = parseInt(pullRequestNumberInput)
}
} else {
throw Error(
`This action is only useful in "pull_request_review" or "workflow_run" triggered runs and you used it in "${eventName}"`
)
}
// PULL REQUEST
const pullRequest = await getPullRequest(
octokit,
owner,
repo,
pullRequestNumber
)
// LABELS
const labelNames = getPullRequestLabels(pullRequest)
// REVIEWS
const [reviews, reviewers, committers] = await getReviews(
octokit,
owner,
repo,
pullRequest.number,
requireCommittersApproval
)
const isApproved = processReviews(
reviews,
reviewers,
committers,
requireCommittersApproval
)
// HANDLE LABEL
let isLabelShouldBeSet = false
let isLabelShouldBeRemoved = false
if (userLabel !== 'not set') {
isLabelShouldBeSet = isApproved && !labelNames.includes(userLabel)
isLabelShouldBeRemoved = !isApproved && labelNames.includes(userLabel)
if (isLabelShouldBeSet) {
await setLabel(octokit, owner, repo, pullRequest.number, userLabel)
if (comment !== '') {
await addComment(octokit, owner, repo, pullRequest.number, comment)
}
} else if (isLabelShouldBeRemoved) {
await removeLabel(octokit, owner, repo, pullRequest.number, userLabel)
}
}
// OUTPUT
verboseOutput('isApproved', String(isApproved))
verboseOutput('labelSet', String(isLabelShouldBeSet))
verboseOutput('labelRemoved', String(isLabelShouldBeRemoved))
}
run()
.then(() =>
core.info(
'\n############### Set Label When Approved complete ##################\n'
)
)
.catch(e => core.setFailed(e.message))