| import * as github from '@actions/github' |
| import * as core from '@actions/core' |
| import * as rest from '@octokit/rest' |
| import {Context} from '@actions/github/lib/context' |
| |
| 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, |
| context: Context, |
| owner: string, |
| repo: string |
| ): Promise<rest.PullsGetResponse> { |
| const pullRequestNumber = context.payload.pull_request |
| ? context.payload.pull_request.number |
| : null |
| if (pullRequestNumber === null) { |
| throw Error(`Could not find PR number in context payload.`) |
| } |
| core.info(`pullRequestNumber: ${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 getWorkflowId( |
| octokit: github.GitHub, |
| runId: number, |
| owner: string, |
| repo: string |
| ): Promise<number> { |
| const reply = await octokit.actions.getWorkflowRun({ |
| owner, |
| repo, |
| // eslint-disable-next-line @typescript-eslint/camelcase |
| run_id: runId |
| }) |
| core.info(`The source run ${runId} is in ${reply.data.workflow_url} workflow`) |
| const workflowIdString = reply.data.workflow_url.split('/').pop() || '' |
| if (!(workflowIdString.length > 0)) { |
| throw new Error('Could not resolve workflow') |
| } |
| return parseInt(workflowIdString) |
| } |
| |
| async function getPrWorkflowRunsIds( |
| octokit: github.GitHub, |
| owner: string, |
| repo: string, |
| branch: string, |
| sha: string, |
| skipRunId: number |
| ): Promise<number[]> { |
| const workflowRuns = await octokit.actions.listRepoWorkflowRuns({ |
| owner, |
| repo, |
| branch, |
| event: 'pull_request', |
| status: 'completed', |
| // eslint-disable-next-line @typescript-eslint/camelcase |
| per_page: 100 |
| }) |
| // may be no need to rerun pending/queued runs |
| const filteredRunsIds: number[] = [] |
| const filteredWorklowRunsIds: number[] = [] |
| |
| for (const workflowRun of workflowRuns.data.workflow_runs) { |
| const workflowId = parseInt( |
| workflowRun.workflow_url.split('/').pop() || '0' |
| ) |
| |
| if ( |
| workflowRun.head_sha === sha && |
| !filteredRunsIds.includes(workflowId) && |
| workflowId !== skipRunId |
| ) { |
| filteredRunsIds.push(workflowId) |
| filteredWorklowRunsIds.push(workflowRun.id) |
| } |
| } |
| |
| return filteredWorklowRunsIds |
| } |
| |
| async function rerunWorkflows( |
| octokit: github.GitHub, |
| owner: string, |
| repo: string, |
| runIds: number[] |
| ): Promise<void> { |
| core.info(`Rerun worklowws: ${runIds}`) |
| for (const runId of runIds) { |
| await octokit.actions.reRunWorkflow({ |
| owner, |
| repo, |
| // eslint-disable-next-line @typescript-eslint/camelcase |
| run_id: runId |
| }) |
| } |
| } |
| |
| async function printDebug( |
| item: object | string | boolean | number, |
| description: string |
| ): Promise<void> { |
| const itemJson = JSON.stringify(item) |
| core.info(`\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 octokit = new github.GitHub(token) |
| const context = github.context |
| const repository = getRequiredEnv('GITHUB_REPOSITORY') |
| const eventName = getRequiredEnv('GITHUB_EVENT_NAME') |
| const selfRunId = parseInt(getRequiredEnv('GITHUB_RUN_ID')) |
| const [owner, repo] = repository.split('/') |
| const selfWorkflowId = await getWorkflowId(octokit, selfRunId, owner, repo) |
| const branch = context.payload.pull_request?.head.ref |
| const sha = context.payload.pull_request?.head.sha |
| |
| core.info( |
| `\n############### Set Label When Approved start ##################\n` + |
| `label: "${userLabel}"\n` + |
| `requireCommittersApproval: ${requireCommittersApproval}\n` + |
| `comment: ${comment}` |
| ) |
| |
| if (eventName !== 'pull_request_review') { |
| throw Error( |
| `This action is only useful in "pull_request_review" triggered runs and you used it in "${eventName}"` |
| ) |
| } |
| |
| // PULL REQUEST |
| const pullRequest = await getPullRequest(octokit, context, owner, repo) |
| |
| // 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) |
| } |
| } |
| |
| //// Future option to rerun workflows if PR approved |
| //// Rerun workflow can have dynamic matrixes which check presence of labels |
| //// it is not possible to rerun successful runs |
| //// https://github.community/t/cannot-re-run-a-successful-workflow-run-using-the-rest-api/123661 |
| // |
| // if (isLabelShouldBeSet) { |
| // const prWorkflowRunsIds = await getPrWorkflowRunsIds( |
| // octokit, |
| // owner, |
| // repo, |
| // branch, |
| // sha, |
| // selfWorkflowId |
| // ) |
| // |
| // await rerunWorkflows(octokit, owner, repo, prWorkflowRunsIds) |
| // } |
| |
| // 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)) |