diff --git a/.github/triage-bot.config.json b/.github/triage-bot.config.json
new file mode 100644
index 0000000000000..1f081d6cd3d21
--- /dev/null
+++ b/.github/triage-bot.config.json
@@ -0,0 +1,21 @@
+{
+ "$schema": "../scripts/triage-bot/triage-bot.schema.json",
+ "params": [
+ {
+ "keyword": "(@fluentui/react-northstar)",
+ "labels": ["Fluent UI react-northstar (v0)"],
+ "assignees": []
+ },
+ { "keyword": "(@fluentui/react)", "labels": ["Fluent UI react (v8)"], "assignees": [] },
+ {
+ "keyword": "(@fluentui/react-components)",
+ "labels": ["Fluent UI react-components (v9)"],
+ "assignees": []
+ },
+ {
+ "keyword": "(@fluentui/web-components)",
+ "labels": ["web-components"],
+ "assignees": ["chrisdholt"]
+ }
+ ]
+}
diff --git a/.github/workflows/issues.yml b/.github/workflows/issues.yml
index 7ca3a414774f4..3abca2b00e3d4 100644
--- a/.github/workflows/issues.yml
+++ b/.github/workflows/issues.yml
@@ -8,13 +8,10 @@ jobs:
triage-issue:
runs-on: ubuntu-latest
steps:
- - uses: Naturalclar/issue-action@v2.0.2
+ - uses: actions/checkout@v2
+ - uses: actions/github-script@v6
with:
- title-or-body: 'body'
- parameters: '[
- {"keywords": ["@fluentui/react-northstar"], "labels": ["Fluent UI react-northstar (v0)"], "assignees": []},
- {"keywords": ["@fluentui/react"], "labels": ["Fluent UI react (v8)"], "assignees": []},
- {"keywords": ["@fluentui/react-components"], "labels": ["Fluent UI react-components (v9)"], "assignees": []},
- {"keywords": ["@fluentui/web-components"],"labels": ["web-components"],"assignees": ["@chrisdholt"]}
- ]'
- github-token: '${{ secrets.GITHUB_TOKEN }}'
+ script: |
+ const config = require('./.github/triage-bot.config.json');
+ const run = require('./scripts/triage-bot');
+ await run({github,context,core,config});
diff --git a/package.json b/package.json
index 1d4342fac2568..7a9392b886930 100644
--- a/package.json
+++ b/package.json
@@ -64,6 +64,8 @@
"update-snapshots": "lage update-snapshots --verbose"
},
"devDependencies": {
+ "@actions/core": "1.9.1",
+ "@actions/github": "5.0.3",
"@azure/data-tables": "13.0.0",
"@babel/core": "7.14.8",
"@babel/plugin-proposal-class-properties": "7.14.5",
diff --git a/scripts/triage-bot/README.md b/scripts/triage-bot/README.md
new file mode 100644
index 0000000000000..d0a4eb1a364ad
--- /dev/null
+++ b/scripts/triage-bot/README.md
@@ -0,0 +1,56 @@
+# Triage Bot
+
+Package for automated issues triage.
+
+> Note: contains logic which needs to be used from within https://github.com/actions/github-script
+
+## Setup
+
+1. Create bot config file `.github/triage-bot.config.json`
+
+```json
+{
+ "$schema": "../scripts/triage-bot/triage-bot.schema.json",
+ "params": [
+ {
+ "keyword": "(@fluentui/react-northstar)",
+ "labels": ["Fluent UI react-northstar (v0)"],
+ "assignees": ["team-1"]
+ },
+ { "keyword": "(@fluentui/react)", "labels": ["Fluent UI react (v8)"], "assignees": ["team-2"] },
+ {
+ "keyword": "(@fluentui/react-components)",
+ "labels": ["Fluent UI react-components (v9)"],
+ "assignees": ["team-3"]
+ }
+ ]
+}
+```
+
+2. Create GH workflow
+
+```yml
+name: Triage Bot
+on:
+ issues:
+ types:
+ - opened
+
+jobs:
+ triage-issue-manual:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v2
+ - uses: actions/github-script@v6
+ with:
+ script: |
+ const config = require('./.github/triage-bot.config.json');
+ const run = require('./scripts/triage-bot');
+ await run({github,context,core,config});
+```
+
+### Outcome
+
+Now on every issue creation, based on project we have chosen, bot will label and add assignees defined by our config automatically
+
+
diff --git a/scripts/triage-bot/index.js b/scripts/triage-bot/index.js
new file mode 100644
index 0000000000000..542bdc98d7ce4
--- /dev/null
+++ b/scripts/triage-bot/index.js
@@ -0,0 +1 @@
+module.exports = require('./triage-bot').main;
diff --git a/scripts/triage-bot/triage-bot.js b/scripts/triage-bot/triage-bot.js
new file mode 100644
index 0000000000000..03195de374c28
--- /dev/null
+++ b/scripts/triage-bot/triage-bot.js
@@ -0,0 +1,81 @@
+/** @typedef {import('./types').Api} Api */
+
+/**
+ *
+ * @param {Api} options
+ */
+async function main(options) {
+ const { context, github, core, config } = options;
+
+ /** @type {string[]} */
+ const logs = [];
+
+ const issue = context.payload.issue;
+ if (!issue) {
+ throw new Error('CONTEXT error. Issue not found');
+ }
+ const issueContent = issue.body?.trim() ?? '';
+
+ if (!issueContent) {
+ core.notice('issue body is empty! Exit early');
+ return;
+ }
+
+ const processedData = config.params.filter(param => {
+ return issueContent.indexOf(param.keyword) !== -1;
+ })[0];
+
+ if (!processedData) {
+ logs.push('No keywords match!');
+ logSummary({ core, logs });
+ return;
+ }
+
+ await callApi({ context, github, data: processedData, logs });
+
+ logSummary({ core, logs });
+}
+
+/**
+ *
+ * @param {{logs:string[];core:import('@actions/core')}} options
+ */
+function logSummary(options) {
+ const { core, logs } = options;
+ core.startGroup('Summary:');
+ logs.forEach(log => core.info(log));
+ core.endGroup();
+}
+
+/**
+ *
+ * @param {Pick & {data: Api['config']['params'][number]; logs:string[]}} options
+ */
+async function callApi(options) {
+ const { context, data, github, logs } = options;
+
+ const labelActionResult = await github.rest.issues.addLabels({
+ issue_number: context.issue.number,
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ labels: data.labels,
+ });
+ if (labelActionResult.status === 200) {
+ logs.push(`Label set: ${data.labels}`);
+ }
+
+ if (data.assignees.length) {
+ const assigneeActionResult = await github.rest.issues.addAssignees({
+ issue_number: context.issue.number,
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ assignees: data.assignees,
+ });
+
+ if (assigneeActionResult.status === 201) {
+ logs.push(`Assignees set: ${data.assignees}`);
+ }
+ }
+}
+
+module.exports = { main };
diff --git a/scripts/triage-bot/triage-bot.schema.json b/scripts/triage-bot/triage-bot.schema.json
new file mode 100644
index 0000000000000..cdd9ae492c111
--- /dev/null
+++ b/scripts/triage-bot/triage-bot.schema.json
@@ -0,0 +1,32 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "title": "Triage Bot Config",
+ "type": "object",
+ "properties": {
+ "params": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "assignees": {
+ "items": {
+ "type": "string"
+ },
+ "type": "array"
+ },
+ "keyword": {
+ "type": "string"
+ },
+ "labels": {
+ "items": {
+ "type": "string"
+ },
+ "type": "array"
+ }
+ },
+ "additionalProperties": false,
+ "required": ["keyword", "labels", "assignees"]
+ }
+ }
+ }
+}
diff --git a/scripts/triage-bot/triage-bot.spec.ts b/scripts/triage-bot/triage-bot.spec.ts
new file mode 100644
index 0000000000000..7ca0f110f315e
--- /dev/null
+++ b/scripts/triage-bot/triage-bot.spec.ts
@@ -0,0 +1,85 @@
+import { main } from './triage-bot';
+import type { GithubScriptsParams } from './types';
+describe(`triage bot`, () => {
+ function setup(options: { issueBody: string }) {
+ const coreSpy = {
+ startGroup: jest.fn(),
+ endGroup: jest.fn(),
+ notice: jest.fn(),
+ info: jest.fn(),
+ };
+ const githubSpy = {
+ rest: { issues: { addLabels: jest.fn(), addAssignees: jest.fn() } },
+ };
+ const contextSpy = {
+ payload: { issue: { body: options.issueBody } },
+ issue: { number: 1 },
+ repo: { owner: 'harold', repo: 'tools' },
+ } as GithubScriptsParams['context'];
+
+ return { core: coreSpy, github: githubSpy, context: contextSpy };
+ }
+ it(`should not do anything if no keyword matches issue content`, async () => {
+ const githubApi = setup({ issueBody: 'This is a bug about hello. For sure its wrong' });
+ const config = {
+ params: [
+ {
+ keyword: 'kekw',
+ labels: ['bug'],
+ assignees: [],
+ },
+ ],
+ };
+
+ await main({ ...((githubApi as unknown) as GithubScriptsParams), config });
+
+ expect(githubApi.github.rest.issues.addLabels).not.toHaveBeenCalled();
+ expect(githubApi.github.rest.issues.addAssignees).not.toHaveBeenCalled();
+ });
+
+ it(`should assign label`, async () => {
+ const githubApi = setup({ issueBody: 'This is a bug about hello. For sure its wrong' });
+ const config = {
+ params: [
+ {
+ keyword: 'hello',
+ labels: ['bug'],
+ assignees: [],
+ },
+ ],
+ };
+ githubApi.github.rest.issues.addLabels.mockReturnValueOnce(Promise.resolve({ status: 200 }));
+
+ await main({ ...((githubApi as unknown) as GithubScriptsParams), config });
+
+ expect(formatMockedCalls(githubApi.core.info.mock.calls)).toMatchInlineSnapshot(`"Label set: bug"`);
+ });
+
+ it(`should assign label and assignees`, async () => {
+ const githubApi = setup({ issueBody: 'This is a bug about hello. For sure its wrong' });
+ const config = {
+ params: [
+ {
+ keyword: 'hello',
+ labels: ['bug'],
+ assignees: ['harold'],
+ },
+ ],
+ };
+ githubApi.github.rest.issues.addLabels.mockReturnValueOnce(Promise.resolve({ status: 200 }));
+ githubApi.github.rest.issues.addAssignees.mockReturnValueOnce(Promise.resolve({ status: 201 }));
+ await main({ ...((githubApi as unknown) as GithubScriptsParams), config });
+
+ expect(formatMockedCalls(githubApi.core.info.mock.calls)).toMatchInlineSnapshot(`
+ "Label set: bug
+ Assignees set: harold"
+ `);
+ });
+});
+
+function formatMockedCalls(values: string[][]) {
+ return values
+ .flat()
+ .map(line => line.trim())
+ .join('\n');
+}
diff --git a/scripts/triage-bot/tsconfig.json b/scripts/triage-bot/tsconfig.json
new file mode 100644
index 0000000000000..4031d4e78fd11
--- /dev/null
+++ b/scripts/triage-bot/tsconfig.json
@@ -0,0 +1,7 @@
+{
+ "extends": "../tsconfig.scripts.json",
+ "compilerOptions": {
+ "types": ["node", "jest"]
+ },
+ "include": ["*"]
+}
diff --git a/scripts/triage-bot/types.ts b/scripts/triage-bot/types.ts
new file mode 100644
index 0000000000000..8df06c5e6b8bf
--- /dev/null
+++ b/scripts/triage-bot/types.ts
@@ -0,0 +1,19 @@
+import * as Core from '@actions/core';
+import Github from '@actions/github';
+
+export interface Api extends GithubScriptsParams {
+ config: Schema;
+}
+export interface GithubScriptsParams {
+ context: typeof Github['context'];
+ github: ReturnType;
+ core: typeof Core;
+}
+
+export interface Schema {
+ params: Array<{
+ keyword: string;
+ labels: string[];
+ assignees: string[];
+ }>;
+}
diff --git a/scripts/tsconfig.json b/scripts/tsconfig.json
index c3e2c670dc574..923f9a4c7d0d9 100644
--- a/scripts/tsconfig.json
+++ b/scripts/tsconfig.json
@@ -23,6 +23,7 @@
"./update-release-notes/**",
"./prettier/**",
"./fluentui-publish/**",
- "./puppeteer/**"
+ "./puppeteer/**",
+ "./triage-bot/**"
]
}
diff --git a/scripts/tsconfig.scripts.json b/scripts/tsconfig.scripts.json
index 4472551b4a6eb..c87a04ae38592 100644
--- a/scripts/tsconfig.scripts.json
+++ b/scripts/tsconfig.scripts.json
@@ -11,6 +11,9 @@
"include": [],
"files": [],
"references": [
+ {
+ "path": "./triage-bot/tsconfig.json"
+ },
{
"path": "./cypress/tsconfig.json"
},
diff --git a/yarn.lock b/yarn.lock
index e60d2f9456055..ed06885ce1903 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2,6 +2,31 @@
# yarn lockfile v1
+"@actions/core@1.9.1":
+ version "1.9.1"
+ resolved "https://registry.yarnpkg.com/@actions/core/-/core-1.9.1.tgz#97c0201b1f9856df4f7c3a375cdcdb0c2a2f750b"
+ integrity sha512-5ad+U2YGrmmiw6du20AQW5XuWo7UKN2052FjSV7MX+Wfjf8sCqcsZe62NfgHys4QI4/Y+vQvLKYL8jWtA1ZBTA==
+ dependencies:
+ "@actions/http-client" "^2.0.1"
+ uuid "^8.3.2"
+
+"@actions/github@5.0.3":
+ version "5.0.3"
+ resolved "https://registry.yarnpkg.com/@actions/github/-/github-5.0.3.tgz#b305765d6173962d113451ea324ff675aa674f35"
+ integrity sha512-myjA/pdLQfhUGLtRZC/J4L1RXOG4o6aYdiEq+zr5wVVKljzbFld+xv10k1FX6IkIJtNxbAq44BdwSNpQ015P0A==
+ dependencies:
+ "@actions/http-client" "^2.0.1"
+ "@octokit/core" "^3.6.0"
+ "@octokit/plugin-paginate-rest" "^2.17.0"
+ "@octokit/plugin-rest-endpoint-methods" "^5.13.0"
+
+"@actions/http-client@^2.0.1":
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/@actions/http-client/-/http-client-2.0.1.tgz#873f4ca98fe32f6839462a6f046332677322f99c"
+ integrity sha512-PIXiMVtz6VvyaRsGY268qvj57hXQEpsYogYOu2nrQhlf+XCGmZstmuZBbAybUl1nQGnvS1k1eEsQ69ZoD7xlSw==
+ dependencies:
+ tunnel "^0.0.6"
+
"@ampproject/remapping@^2.1.0":
version "2.2.0"
resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.2.0.tgz#56c133824780de3174aed5ab6834f3026790154d"
@@ -3386,7 +3411,7 @@
dependencies:
"@octokit/types" "^6.0.3"
-"@octokit/core@^3.5.1":
+"@octokit/core@^3.5.1", "@octokit/core@^3.6.0":
version "3.6.0"
resolved "https://registry.yarnpkg.com/@octokit/core/-/core-3.6.0.tgz#3376cb9f3008d9b3d110370d90e0a1fcd5fe6085"
integrity sha512-7RKRKuA4xTjMhY+eG3jthb3hlZCsOwg3rztWh75Xc+ShDWOfDDATWbeZpAHBNRpm4Tv9WgBMOy1zEJYXG6NJ7Q==
@@ -3431,6 +3456,11 @@
resolved "https://registry.yarnpkg.com/@octokit/openapi-types/-/openapi-types-11.2.0.tgz#b38d7fc3736d52a1e96b230c1ccd4a58a2f400a6"
integrity sha512-PBsVO+15KSlGmiI8QAzaqvsNlZlrDlyAJYcrXBCvVUxCp7VnXjkwPoFHgjEJXx3WF9BAwkA6nfCUA7i9sODzKA==
+"@octokit/openapi-types@^12.11.0":
+ version "12.11.0"
+ resolved "https://registry.yarnpkg.com/@octokit/openapi-types/-/openapi-types-12.11.0.tgz#da5638d64f2b919bca89ce6602d059f1b52d3ef0"
+ integrity sha512-VsXyi8peyRq9PqIz/tpqiL2w3w80OgVMwBHltTml3LmVvXiphgeqmY9mvBw9Wu7e0QWk/fqD37ux8yP5uVekyQ==
+
"@octokit/plugin-enterprise-rest@^6.0.1":
version "6.0.1"
resolved "https://registry.yarnpkg.com/@octokit/plugin-enterprise-rest/-/plugin-enterprise-rest-6.0.1.tgz#e07896739618dab8da7d4077c658003775f95437"
@@ -3450,6 +3480,13 @@
dependencies:
"@octokit/types" "^6.34.0"
+"@octokit/plugin-paginate-rest@^2.17.0":
+ version "2.21.3"
+ resolved "https://registry.yarnpkg.com/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-2.21.3.tgz#7f12532797775640dbb8224da577da7dc210c87e"
+ integrity sha512-aCZTEf0y2h3OLbrgKkrfFdjRL6eSOo8komneVQJnYecAxIej7Bafor2xhuDJOIFau4pk0i/P28/XgtbyPF0ZHw==
+ dependencies:
+ "@octokit/types" "^6.40.0"
+
"@octokit/plugin-request-log@^1.0.0":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@octokit/plugin-request-log/-/plugin-request-log-1.0.0.tgz#eef87a431300f6148c39a7f75f8cfeb218b2547e"
@@ -3476,6 +3513,14 @@
"@octokit/types" "^6.34.0"
deprecation "^2.3.1"
+"@octokit/plugin-rest-endpoint-methods@^5.13.0":
+ version "5.16.2"
+ resolved "https://registry.yarnpkg.com/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-5.16.2.tgz#7ee8bf586df97dd6868cf68f641354e908c25342"
+ integrity sha512-8QFz29Fg5jDuTPXVtey05BLm7OB+M8fnvE64RNegzX7U+5NUXcOcnpTIK0YfSHBg8gYd0oxIq3IZTe9SfPZiRw==
+ dependencies:
+ "@octokit/types" "^6.39.0"
+ deprecation "^2.3.1"
+
"@octokit/request-error@^1.0.1", "@octokit/request-error@^1.0.2":
version "1.0.4"
resolved "https://registry.yarnpkg.com/@octokit/request-error/-/request-error-1.0.4.tgz#15e1dc22123ba4a9a4391914d80ec1e5303a23be"
@@ -3565,6 +3610,13 @@
dependencies:
"@octokit/openapi-types" "^11.2.0"
+"@octokit/types@^6.39.0", "@octokit/types@^6.40.0":
+ version "6.41.0"
+ resolved "https://registry.yarnpkg.com/@octokit/types/-/types-6.41.0.tgz#e58ef78d78596d2fb7df9c6259802464b5f84a04"
+ integrity sha512-eJ2jbzjdijiL3B4PrSQaSjuF2sPEQPVCPzBvTHJD9Nz+9dw2SGH4K4xeQJ77YfTq5bRQ+bD8wT11JbeDPmxmGg==
+ dependencies:
+ "@octokit/openapi-types" "^12.11.0"
+
"@opencensus/web-types@0.0.7":
version "0.0.7"
resolved "https://registry.yarnpkg.com/@opencensus/web-types/-/web-types-0.0.7.tgz#4426de1fe5aa8f624db395d2152b902874f0570a"