Skip to content

Commit

Permalink
feat(project): add issue triage workflow and action (carbon-design-sy…
Browse files Browse the repository at this point in the history
…stem#6504)

* feat(project): add issue triage workflow and action

* chore(actions): clean-up issue action

Co-authored-by: TJ Egan <[email protected]>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
  • Loading branch information
3 people authored Jul 24, 2020
1 parent b8ad101 commit bf35eeb
Show file tree
Hide file tree
Showing 13 changed files with 387 additions and 4 deletions.
15 changes: 15 additions & 0 deletions .github/workflows/issue-triage.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
name: Issue Triage
on:
issues:
types: [opened]
issue_comment:
types: [created]
jobs:
comment:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@master
- uses: ./actions/issues
with:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
enabled: true
2 changes: 1 addition & 1 deletion actions/add-review-labels/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@carbon/actions-add-review-labels",
"private": true,
"version": "0.3.0",
"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",
Expand Down
49 changes: 49 additions & 0 deletions actions/issues/ARCHITECTURE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# Issues action

## Overview

The issues action is responsible for adding appropriate labels during the triage
process. The action is currently executed through
[`./src/run.js`](./src/run.js).

This entrypoint will load up the plugins available from the
[`plugins` directory](./src/plugins) and run them for the given workflow events.
The most relevant events include the following:

```yml
on:
issues:
types: [opened]
issue_comment:
types: [created]
```
## Plugins
Each process for adding labels is separated out into a `plugin`. A plugin takes
on the following shape:

```ts
interface ActionPlugin {
name: string;
conditions?: [ActionPluginCondition];
run: (context: GitHubActionContext, octokit: Octokit) => Promise<void>;
}
interface ActionPluginCondition {
key: string;
run: (context: GitHubActionContext, octokit: Octokit) => Promise<void>;
}
```

Each `plugin` may specify a set of `conditions` that must be true for the plugin
to run. Common conditions are available in
[`./src/conditions.js`](./src/conditions.js), including:

- Only run when an issue is open
- Only run when a comment is created
- Only run when an issue has a specific label

When a `plugin` is run, it is given the `context` for the action, along with an
optional instance of `octokit` to use for API requests. A plugin can do any
operations needed during its lifecycle to the issue in question.
6 changes: 6 additions & 0 deletions actions/issues/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/src/run.js"]
12 changes: 12 additions & 0 deletions actions/issues/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
name: Issue Triage
description: A custom action for triaging issues
inputs:
GITHUB_TOKEN:
description: A GitHub token to execute GitHub tasks
required: true
enabled:
description: Specify whether this action should run
required: false
runs:
using: 'docker'
image: 'Dockerfile'
9 changes: 9 additions & 0 deletions actions/issues/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"name": "@carbon/actions-issues",
"private": true,
"version": "0.0.0",
"dependencies": {
"@actions/core": "^1.2.3",
"@actions/github": "^2.1.1"
}
}
73 changes: 73 additions & 0 deletions actions/issues/src/conditions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/**
* 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.
*/

const events = {
issues: {
opened: {
key: 'issue_opened',
run: action('opened'),
},
},
comments: {
created: {
key: 'comment_created',
run: action('created'),
},
},
};

const states = {
issues: {
from_non_collaborator: {
key: 'from_non_collaborator',
run(context) {
const { issue } = context.payload;
const roles = new Set(['OWNER', 'COLLABORATOR']);
return roles.has(issue.author_association);
},
},
open: {
key: 'issue_is_open',
run(context) {
return !context.issue.closed_at;
},
},
closed: {
key: 'issue_is_closed',
run(context) {
return !!context.issue.closed_at;
},
},
has(label) {
return {
key: `has_issue_label_${label}`,
run(context) {
if (!context.payload.issue) {
return;
}
return context.payload.issue.labels.find(({ name }) => {
return name === label;
});
},
};
},
},
};

/**
* Check if a specific action was triggered for a given action context
* @param {string} name
* @returns {Function}
*/
function action(name) {
return (context) => context.payload.action === name;
}

module.exports = {
events,
states,
};
22 changes: 22 additions & 0 deletions actions/issues/src/labels.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/**
* 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';

module.exports = {
status: {
// Reviews
oneMoreReview: 'status: one more review 👀',
readyForReview: 'status: ready for review 👀',
readyToMerge: 'status: ready to merge 🎉',

// Triage
needsTriage: 'status: needs triage 🕵️‍♀️',
waitingForAuthor: `status: waiting for author's response 💬`,
waitingForMaintainer: 'status: waiting for maintainer response 💬',
},
};
84 changes: 84 additions & 0 deletions actions/issues/src/plugins/add-issue-response.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/**
* 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 { events, states } = require('../conditions');
const labels = require('../labels');

const { needsTriage, waitingForAuthor, waitingForMaintainer } = labels.status;

const plugin = {
name: 'Add triage response',
conditions: [
events.comments.created,
states.issues.open,
states.issues.has(needsTriage),
],
async run(context, octokit) {
const { comment, issue, repository } = context.payload;
const roles = new Set(['OWNER', 'COLLABORATOR']);

// waiting for author's response
if (roles.has(comment.author_association)) {
const hasMaintainerLabel = issue.labels.find((label) => {
return label.name === waitingForMaintainer;
});
if (hasMaintainerLabel) {
await octokit.issues.removeLabel({
owner: repository.owner.login,
repo: repository.name,
issue_number: issue.number,
name: waitingForMaintainer,
});
}

const hasAuthorLabel = issue.labels.find((label) => {
return label.name === waitingForAuthor;
});

if (hasAuthorLabel) {
return;
}

await octokit.issues.addLabels({
owner: repository.owner.login,
repo: repository.name,
issue_number: issue.number,
labels: [waitingForAuthor],
});
} else {
const hasAuthorLabel = issue.labels.find((label) => {
return label.name === waitingForAuthor;
});
if (hasAuthorLabel) {
await octokit.issues.removeLabel({
owner: repository.owner.login,
repo: repository.name,
issue_number: issue.number,
name: waitingForAuthor,
});
}

const hasMaintainerLabel = issue.labels.find((label) => {
return label.name === waitingForMaintainer;
});
if (hasMaintainerLabel) {
return;
}

await octokit.issues.addLabels({
owner: repository.owner.login,
repo: repository.name,
issue_number: issue.number,
labels: [waitingForMaintainer],
});
}
},
};

module.exports = plugin;
42 changes: 42 additions & 0 deletions actions/issues/src/plugins/add-triage-label.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/**
* 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 core = require('@actions/core');
const { events } = require('../conditions');
const labels = require('../labels');

const plugin = {
name: 'Add triage label',
conditions: [events.issues.opened],
async run(context, octokit) {
const { issue, repository } = context.payload;
const roles = new Set(['OWNER', 'COLLABORATOR']);
if (roles.has(issue.author_association)) {
core.info(
'Issue opened by project collaborator. No triage label necessary'
);
return;
}

const hasTriageLabel = issue.labels.find((label) => {
return label.name === labels.status.needsTriage;
});

if (!hasTriageLabel) {
await octokit.issues.addLabels({
owner: repository.owner.login,
repo: repository.name,
issue_number: issue.number,
labels: [labels.status.needsTriage],
});
}
},
};

module.exports = plugin;
12 changes: 12 additions & 0 deletions actions/issues/src/plugins/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/**
* 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';

module.exports = {
plugins: [require('./add-triage-label'), require('./add-issue-response')],
};
Loading

0 comments on commit bf35eeb

Please sign in to comment.