Skip to content

Commit

Permalink
Handle reviews
Browse files Browse the repository at this point in the history
  • Loading branch information
Tobiasz Kędzierski committed Oct 26, 2020
1 parent ab32655 commit 47b5dec
Show file tree
Hide file tree
Showing 6 changed files with 328 additions and 47 deletions.
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@

The MIT License (MIT)

Copyright (c) 2018 GitHub, Inc. and contributors
Copyright (c) 2020 GitHub, Inc. and contributors

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
57 changes: 52 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,26 +13,73 @@
- [Inputs and outputs](#inputs-and-outputs)
- [Inputs](#inputs)
- [Outputs](#outputs)
- [Examples](#examples)
- [Workflow Run event](#workflow-run-event)
- [Development environment](#development-environment)
- [License](#license)

<!-- END doctoc generated TOC please keep comment here to allow auto update -->

# Context and motivation

TODO
Label When Approved is an action that checks is Pull Request is approved and assign label to it.
Label is not set or removed when Pull Request has awaiting requested changes.

Setting label is optional that only output can be used in the workflow.

The required input `require_committers_approval` says is approval can be done by people with read access to the repo
or by anyone. It may be useful in repositories which requires committers approvals like [Apache Software Foundation](https://github.com/apache/)
projects.

# Inputs and outputs

## Inputs

| Input | Required | Default | Comment |
|-------------------------|----------|--------------|-----------------------------------------------------------------------------------------------------|
| `token` | yes | | The github token passed from `${{ secrets.GITHUB_TOKEN }}` |
| Input | Required | Example | Comment |
|-------------------------------|----------|-------------------------------|-------------------------------------------------------------------------|
| `token` | yes | `${{ secrets.GITHUB_TOKEN }}` | The github token passed from `${{ secrets.GITHUB_TOKEN }}` |
| `label` | no | `Approved by committers` | Label to be added/removed to the Pull Request if approved/not approved |
| `require_committers_approval` | no | `true` | Is approval from user with write permission required |

## Outputs

TODO
| Output | |
|----------------|------------------------------|
| `isApproved` | is Pull Reqeuest is approved |
| `labelSet` | is label was set |
| `labelRemoved` | is label was removed |

# Examples

### Workflow Run event

```yaml
name: Label when approved
on: pull_request_review

jobs:

label-when-approved:
name: "Label when approved"
runs-on: ubuntu-latest
outputs:
isApprovedByCommiters: ${{ steps.label-when-approved-by-commiters.outputs.isApproved }}
isApprovedByAnyone: ${{ steps.label-when-approved-by-anyone.outputs.isApproved }}
steps:
- name: Label when approved by commiters
uses: TobKed/[email protected]
id: label-when-approved-by-commiters
with:
token: ${{ secrets.GITHUB_TOKEN }}
label: 'ready to merge (committers)'
require_committers_approval: 'true'
- name: Label when approved by anyone
uses: TobKed/[email protected]
id: label-when-approved-by-anyone
with:
token: ${{ secrets.GITHUB_TOKEN }}
```
## Development environment
Expand Down
5 changes: 3 additions & 2 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,11 @@ inputs:
required: true
label:
description: A label to be checked/added/removed
required: true
required: false
require_committers_approval:
description: Is approval from person with write access to repo required
required: true
required: false
default: 'false'
runs:
using: 'node12'
main: 'dist/index.js'
Expand Down
137 changes: 120 additions & 17 deletions dist/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -1466,6 +1466,10 @@ function getRequiredEnv(key) {
}
return value;
}
function verboseOutput(name, value) {
core.info(`Setting output: ${name}: ${value}`);
core.setOutput(name, value);
}
function getPullRequest(octokit, context, owner, repo) {
return __awaiter(this, void 0, void 0, function* () {
const pullRequestNumber = context.payload.pull_request
Expand All @@ -1492,31 +1496,38 @@ function getPullRequestLabels(pullRequest) {
}
function getReviews(octokit, owner, repo, number, getComitters) {
return __awaiter(this, void 0, void 0, function* () {
const reviews = yield octokit.pulls.listReviews({
let reviews = [];
const options = octokit.pulls.listReviews.endpoint.merge({
owner,
repo,
// eslint-disable-next-line @typescript-eslint/camelcase
pull_number: number
});
const reviewers = reviews ? reviews.data.map(review => review.user.login) : [];
yield octokit.paginate(options).then(r => {
reviews = r;
});
const reviewers = reviews ? reviews.map(review => review.user.login) : [];
const reviewersAlreadyChecked = [];
const committers = [];
if (getComitters) {
core.info('Checking reviewers permissions:');
for (const reviewer of reviewers) {
if (!committers.includes(reviewer)) {
if (!reviewersAlreadyChecked.includes(reviewer)) {
const p = yield octokit.repos.getCollaboratorPermissionLevel({
owner,
repo,
username: reviewer
});
const permission = p.data.permission;
core.info(`\nChecking: "${reviewer}" permissions: ${permission}.\n`);
if (permission === 'admin' || permission === 'write') {
committers.push(reviewer);
}
core.info(`\t${reviewer}: ${permission}`);
reviewersAlreadyChecked.push(reviewer);
}
}
}
return [reviews.data, reviewers, committers];
return [reviews, reviewers, committers];
});
}
function processReviews(reviews, reviewers, committers, requireCommittersApproval) {
Expand All @@ -1532,9 +1543,9 @@ function processReviews(reviews, reviewers, committers, requireCommittersApprova
}
}
}
core.info(`User reviews:`);
core.info(`Reviews:`);
for (const user in reviewStates) {
core.info(`User "${user}" : "${reviewStates[user]}"`);
core.info(`\t${user}: ${reviewStates[user].toLowerCase()}`);
}
for (const user in reviewStates) {
if (reviewStates[user] === 'APPROVED') {
Expand All @@ -1552,6 +1563,7 @@ function processReviews(reviews, reviewers, committers, requireCommittersApprova
}
function setLabel(octokit, owner, repo, pullRequestNumber, label) {
return __awaiter(this, void 0, void 0, function* () {
core.info(`Setting label: ${label}`);
yield octokit.issues.addLabels({
// eslint-disable-next-line @typescript-eslint/camelcase
issue_number: pullRequestNumber,
Expand All @@ -1563,6 +1575,7 @@ function setLabel(octokit, owner, repo, pullRequestNumber, label) {
}
function removeLabel(octokit, owner, repo, pullRequestNumber, label) {
return __awaiter(this, void 0, void 0, function* () {
core.info(`Removing label: ${label}`);
yield octokit.issues.removeLabel({
// eslint-disable-next-line @typescript-eslint/camelcase
issue_number: pullRequestNumber,
Expand All @@ -1572,24 +1585,87 @@ function removeLabel(octokit, owner, repo, pullRequestNumber, label) {
});
});
}
function getWorkflowId(octokit, runId, owner, repo) {
return __awaiter(this, void 0, void 0, function* () {
const reply = yield 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);
});
}
function getPrWorkflowRunsIds(octokit, owner, repo, branch, sha, skipRunId) {
return __awaiter(this, void 0, void 0, function* () {
const workflowRuns = yield 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 = [];
const filteredWorklowRunsIds = [];
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;
});
}
function rerunWorkflows(octokit, owner, repo, runIds) {
return __awaiter(this, void 0, void 0, function* () {
core.info(`Rerun worklowws: ${runIds}`);
for (const runId of runIds) {
yield octokit.actions.reRunWorkflow({
owner,
repo,
// eslint-disable-next-line @typescript-eslint/camelcase
run_id: runId
});
}
});
}
function printDebug(item, description) {
return __awaiter(this, void 0, void 0, function* () {
const itemJson = JSON.stringify(item);
core.info(`\n ######### ${description} ######### \n: ${itemJson}\n\n`);
});
}
function run() {
var _a, _b;
return __awaiter(this, void 0, void 0, function* () {
const token = core.getInput('token', { required: true });
const userLabel = core.getInput('label', { required: true });
const userLabel = core.getInput('label', { required: false }) || 'not set';
const requireCommittersApproval = core.getInput('require_committers_approval', {
required: true
});
required: false
}) === 'true';
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 = yield getWorkflowId(octokit, selfRunId, owner, repo);
const branch = (_a = context.payload.pull_request) === null || _a === void 0 ? void 0 : _a.head.ref;
const sha = (_b = context.payload.pull_request) === null || _b === void 0 ? void 0 : _b.head.sha;
core.info(`\n############### Set Label When Approved start ##################\n` +
`label: "${userLabel}"\n` +
`requireCommittersApproval: ${requireCommittersApproval}`);
if (eventName !== 'pull_request_review') {
throw Error(`This action is only useful in "pull_request_review" triggered runs and you used it in "${eventName}"`);
}
Expand All @@ -1598,15 +1674,42 @@ function run() {
// LABELS
const labelNames = getPullRequestLabels(pullRequest);
// REVIEWS
const [reviews, reviewers, committers] = yield getReviews(octokit, owner, repo, pullRequest.number, requireCommittersApproval === 'true');
const isApproved = processReviews(reviews, reviewers, committers, requireCommittersApproval === 'true');
const [reviews, reviewers, committers] = yield getReviews(octokit, owner, repo, pullRequest.number, requireCommittersApproval);
const isApproved = processReviews(reviews, reviewers, committers, requireCommittersApproval);
// HANDLE LABEL
if (isApproved && !labelNames.includes(userLabel)) {
setLabel(octokit, owner, repo, pullRequest.number, userLabel);
}
else if (!isApproved && labelNames.includes(userLabel)) {
removeLabel(octokit, owner, repo, pullRequest.number, userLabel);
let isLabelShouldBeSet = false;
let isLabelShouldBeRemoved = false;
if (userLabel !== 'not set') {
isLabelShouldBeSet = isApproved && !labelNames.includes(userLabel);
isLabelShouldBeRemoved = !isApproved && labelNames.includes(userLabel);
if (isLabelShouldBeSet) {
setLabel(octokit, owner, repo, pullRequest.number, userLabel);
}
else if (isLabelShouldBeRemoved) {
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()
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
"node",
"setup"
],
"author": "YourNameOrOrganization",
"author": "Tobiasz Kedzierski",
"license": "MIT",
"dependencies": {
"@actions/core": "^1.2.2",
Expand Down
Loading

0 comments on commit 47b5dec

Please sign in to comment.