Skip to content

Commit

Permalink
chore(github): add workflow and action for automating review labels (#…
Browse files Browse the repository at this point in the history
…5649)

* chore(github): add workflow and action for automating review labels

* feat(cli): add support for multiple package roots

* chore(actions): run sync command for package files

* Update actions/add-review-labels/index.js

* Update actions/add-review-labels/index.js

* Update actions/add-review-labels/index.js

* fix(actions): update logic for adding ready label

* chore(project): update eslint to check in actions folder
  • Loading branch information
joshblack authored and Alessandra Davila committed Mar 25, 2020
1 parent 065bd00 commit f9da6c2
Show file tree
Hide file tree
Showing 24 changed files with 387 additions and 57 deletions.
11 changes: 11 additions & 0 deletions .github/workflows/add-review-labels.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
name: Add Review Labels
on: pull_request_review
jobs:
reviewer:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@master
- uses: ./actions/add-review-labels
with:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
AUTO_LABEL_USERS: 'asudoh,emyarod,tw15egan'
Binary file added .yarn/offline-mirror/@actions-core-1.2.3.tgz
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file added .yarn/offline-mirror/@octokit-graphql-4.3.1.tgz
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file added .yarn/offline-mirror/@octokit-request-5.3.2.tgz
Binary file not shown.
Binary file added .yarn/offline-mirror/@octokit-rest-16.43.1.tgz
Binary file not shown.
Binary file added .yarn/offline-mirror/@octokit-types-2.5.0.tgz
Binary file not shown.
Binary file added .yarn/offline-mirror/fast-glob-3.2.2.tgz
Binary file not shown.
Binary file added .yarn/offline-mirror/tunnel-0.0.6.tgz
Binary file not shown.
Binary file not shown.
4 changes: 4 additions & 0 deletions actions/add-review-labels/.npmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
**/__mocks__/**
**/__tests__/**
**/examples/**
**/tasks/**
6 changes: 6 additions & 0 deletions actions/add-review-labels/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
FROM node:slim

WORKDIR /usr/src/action
COPY . .
RUN yarn install --production
ENTRYPOINT ["node", "/usr/src/action/index.js"]
12 changes: 12 additions & 0 deletions actions/add-review-labels/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
name: Add review labels
description: A custom action that adds review labels to a Pull Request
inputs:
GITHUB_TOKEN:
description: A GitHub token to execute GitHub tasks
required: true
AUTO_LABEL_USERS:
description:
Opt-in users who want the ready to merge label automatically applied
runs:
using: 'docker'
image: 'Dockerfile'
162 changes: 162 additions & 0 deletions actions/add-review-labels/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
/**
* Copyright IBM Corp. 2020, 2020
*
* This source code is licensed under the Apache-2.0 license found in the
* LICENSE file in the root directory of this source tree.
*/

'use strict';

const github = require('@actions/github');
const core = require('@actions/core');

async function run() {
const { context } = github;
const token = core.getInput('GITHUB_TOKEN', {
required: true,
});
const autoLabelUsers = core.getInput('AUTO_LABEL_USERS').split(',');
const octokit = new github.GitHub(token);
const { pull_request: pullRequest, repository, review } = context.payload;
const { state, draft } = pullRequest;

// We only want to work with Pull Requests that are marked as open
if (state !== 'open') {
return;
}

// We only want to work with Pull Requests that are not draft PRs
if (draft) {
return;
}

const {
data: permissionLevel,
} = await octokit.repos.getCollaboratorPermissionLevel({
owner: repository.owner.login,
repo: repository.name,
username: review.user.login,
});

// If the reviewer doesn't have one of the following permission levels
// then ignore the event
const acceptedPermissionLevels = new Set(['admin', 'write']);
if (!acceptedPermissionLevels.has(permissionLevel.permission)) {
return;
}

// If the review was not an approval then we'll ignore the event
if (review.state !== 'approved') {
return;
}

const { data: allReviews } = await octokit.pulls.listReviews({
owner: repository.owner.login,
repo: repository.name,
pull_number: pullRequest.number,
per_page: 100,
});

// The `listReviews` endpoint will return all of the reviews for the pull
// request. We only care about the most recent reviews so we'll go through the
// list and get the most recent review for each reviewer
const reviewers = {};
const reviews = [];

// Process reviews in reverse order since they are listed from oldest to newest
for (const review of allReviews.reverse()) {
const { author_association: association, user } = review;
// If we've already saved a review for this user we already have the most
// recent review
if (reviewers[user.login]) {
continue;
}

// If the author of the review is not a collaborator we ignore it
if (association !== 'COLLABORATOR') {
continue;
}

reviewers[user.login] = true;
reviews.push(review);
}

const approved = reviews.filter(review => {
return review.state === 'APPROVED';
});

const additionalReviewLabel = 'status: one more review 👀';
const readyForReviewLabel = 'status: ready for review 👀';
const readyToMergeLabel = 'status: ready to merge 🎉';
const visualReviewLabel = 'status: visual review 🔍';
const contentReviewLabel = 'status: content review ✍️';
const needsReviewLabels = new Set([visualReviewLabel, contentReviewLabel]);

if (approved.length > 0) {
const hasReadyLabel = pullRequest.labels.find(label => {
return label.name === readyForReviewLabel;
});
if (hasReadyLabel) {
await octokit.issues.removeLabel({
owner: repository.owner.login,
repo: repository.name,
issue_number: pullRequest.number,
name: readyForReviewLabel,
});
}
}

if (approved.length === 1) {
const hasAdditionalReviewLabel = pullRequest.labels.find(label => {
return label.name === additionalReviewLabel;
});
if (!hasAdditionalReviewLabel) {
await octokit.issues.addLabels({
owner: repository.owner.login,
repo: repository.name,
issue_number: pullRequest.number,
labels: [additionalReviewLabel],
});
}
return;
}

if (approved.length >= 2) {
const hasAdditionalReviewLabel = pullRequest.labels.find(label => {
return label.name === additionalReviewLabel;
});
if (hasAdditionalReviewLabel) {
await octokit.issues.removeLabel({
owner: repository.owner.login,
repo: repository.name,
issue_number: pullRequest.number,
name: additionalReviewLabel,
});
}

const allNeedsReviewLabels = pullRequest.labels.filter(label => {
return needsReviewLabels.has(label.name);
});
if (allNeedsReviewLabels.length > 0) {
return;
}

const shouldAutoLabel = autoLabelUsers.find(user => {
return user === pullRequest.user.login;
});
if (shouldAutoLabel) {
await octokit.issues.addLabels({
owner: repository.owner.login,
repo: repository.name,
issue_number: pullRequest.number,
labels: [readyToMergeLabel],
});
}
return;
}
}

run().catch(error => {
console.log(error);
process.exit(1);
});
20 changes: 20 additions & 0 deletions actions/add-review-labels/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"name": "@carbon/actions-add-review-labels",
"private": true,
"version": "0.0.0",
"license": "Apache-2.0",
"main": "index.js",
"repository": "https://github.com/carbon-design-system/carbon/tree/master/actions/add-review-labels",
"bugs": "https://github.com/carbon-design-system/carbon/issues",
"keywords": [
"ibm",
"carbon",
"carbon-design-system",
"components",
"react"
],
"dependencies": {
"@actions/core": "^1.2.3",
"@actions/github": "^2.1.1"
}
}
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
},
"workspaces": {
"packages": [
"packages/*"
"packages/*",
"actions/*"
],
"nohoist": []
},
Expand All @@ -20,7 +21,7 @@
"doctoc": "doctoc --title '## Table of Contents'",
"format": "prettier --write '**/*.{js,md,scss,ts}' '!**/{build,es,lib,storybook,ts,umd}/**'",
"format:diff": "prettier --list-different '**/*.{js,md,scss,ts}' '!**/{build,es,lib,storybook,ts,umd}/**' '!packages/components/**'",
"lint": "eslint packages",
"lint": "eslint packages actions",
"lint:docs": "alex 'docs/**/*.md' -q",
"lint:styles": "stylelint '**/*.{css,scss}' --config ./packages/stylelint-config-elements",
"sync": "carbon-cli sync",
Expand Down
1 change: 1 addition & 0 deletions packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"chalk": "^2.4.2",
"child-process-promise": "^2.2.1",
"clipboardy": "^2.1.0",
"fast-glob": "^3.2.2",
"fs-extra": "^8.0.1",
"inquirer": "^6.4.1",
"prettier": "^1.19.1",
Expand Down
65 changes: 35 additions & 30 deletions packages/cli/src/commands/sync/package.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
const fs = require('fs-extra');

const REPO_URL_BASE =
'https://github.com/carbon-design-system/carbon/tree/master/packages';
'https://github.com/carbon-design-system/carbon/tree/master';

// This is our default set of keywords to include in each `package.json` packageJson
const DEFAULT_KEYWORDS = [
Expand Down Expand Up @@ -71,39 +71,44 @@ function sortFields(a, b) {

function run({ packagePaths }) {
return Promise.all(
packagePaths.map(async ({ basename, packageJsonPath, packageJson }) => {
packageJson.repository = `${REPO_URL_BASE}/${basename}`;
packageJson.bugs =
'https://github.com/carbon-design-system/carbon/issues';
packageJson.license = 'Apache-2.0';
packageJson.publishConfig = {
access: 'public',
};
packagePaths.map(
async ({ packageJsonPath, packageJson, packageFolder }) => {
packageJson.repository = `${REPO_URL_BASE}/${packageFolder}`;
packageJson.bugs =
'https://github.com/carbon-design-system/carbon/issues';
packageJson.license = 'Apache-2.0';

if (Array.isArray(packageJson.keywords)) {
const keywordsToAdd = DEFAULT_KEYWORDS.filter(keyword => {
return packageJson.keywords.indexOf(keyword) === -1;
});
if (keywordsToAdd.length > 0) {
packageJson.keywords = [...packageJson.keywords, ...keywordsToAdd];
if (!packageJson.private) {
packageJson.publishConfig = {
access: 'public',
};
}

if (Array.isArray(packageJson.keywords)) {
const keywordsToAdd = DEFAULT_KEYWORDS.filter(keyword => {
return packageJson.keywords.indexOf(keyword) === -1;
});
if (keywordsToAdd.length > 0) {
packageJson.keywords = [...packageJson.keywords, ...keywordsToAdd];
}
} else {
packageJson.keywords = DEFAULT_KEYWORDS;
}
} else {
packageJson.keywords = DEFAULT_KEYWORDS;
}

// Construct our new packageJson packageJson with sorted fields
const file = Object.keys(packageJson)
.sort(sortFields)
.reduce(
(acc, key) => ({
...acc,
[key]: packageJson[key],
}),
{}
);
// Construct our new packageJson packageJson with sorted fields
const file = Object.keys(packageJson)
.sort(sortFields)
.reduce(
(acc, key) => ({
...acc,
[key]: packageJson[key],
}),
{}
);

await fs.writeJson(packageJsonPath, file, { spaces: 2 });
})
await fs.writeJson(packageJsonPath, file, { spaces: 2 });
}
)
);
}

Expand Down
37 changes: 14 additions & 23 deletions packages/cli/src/workspace.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,42 +9,33 @@

const execa = require('execa');
const fs = require('fs-extra');
const glob = require('fast-glob');
const path = require('path');
const packageJson = require('../../../package.json');

const denylist = new Set(['.DS_Store']);
const PACKAGES_DIR = path.resolve(__dirname, '../..');
const packagePaths = fs
.readdirSync(PACKAGES_DIR)
.filter(basename => {
const filename = path.join(PACKAGES_DIR, basename);
if (denylist.has(filename)) {
return false;
}

const stats = fs.lstatSync(filename);
if (!stats.isDirectory()) {
return false;
}

return true;
})
.map(pkg => {
const packageJsonPath = path.join(PACKAGES_DIR, pkg, 'package.json');
const WORKSPACE_ROOT = path.resolve(__dirname, '../../../');
const packagePaths = glob
.sync(
packageJson.workspaces.packages.map(pattern => `${pattern}/package.json`)
)
.map(match => {
const packageJsonPath = path.join(WORKSPACE_ROOT, match);
return {
basename: pkg,
packageJsonPath,
packageJson: fs.readJsonSync(packageJsonPath),
packagePath: path.join(PACKAGES_DIR, pkg),
packagePath: path.dirname(packageJsonPath),
packageFolder: path.relative(
WORKSPACE_ROOT,
path.dirname(packageJsonPath)
),
};
});

const env = {
root: {
directory: path.resolve(__dirname, '../../..'),
directory: WORKSPACE_ROOT,
packageJson,
},
packagesDir: PACKAGES_DIR,
packagePaths,
};

Expand Down
Loading

0 comments on commit f9da6c2

Please sign in to comment.