From 573a16adac4fd8906c8fdfa82f78f7aee023f2c6 Mon Sep 17 00:00:00 2001 From: Martin Hochel Date: Thu, 22 Sep 2022 19:57:43 +0200 Subject: [PATCH 1/3] feat(scripts): implement triage-bot module --- scripts/triage-bot/README.md | 56 +++++++++++++++ scripts/triage-bot/index.js | 1 + scripts/triage-bot/triage-bot.js | 81 +++++++++++++++++++++ scripts/triage-bot/triage-bot.schema.json | 32 +++++++++ scripts/triage-bot/triage-bot.spec.ts | 85 +++++++++++++++++++++++ scripts/triage-bot/tsconfig.json | 7 ++ scripts/triage-bot/types.ts | 19 +++++ scripts/tsconfig.json | 3 +- scripts/tsconfig.scripts.json | 3 + 9 files changed, 286 insertions(+), 1 deletion(-) create mode 100644 scripts/triage-bot/README.md create mode 100644 scripts/triage-bot/index.js create mode 100644 scripts/triage-bot/triage-bot.js create mode 100644 scripts/triage-bot/triage-bot.schema.json create mode 100644 scripts/triage-bot/triage-bot.spec.ts create mode 100644 scripts/triage-bot/tsconfig.json create mode 100644 scripts/triage-bot/types.ts 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 + +picking library during issue creation 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" }, From 971121ebe76e7a3ce79626cf807239e1b3e9e940 Mon Sep 17 00:00:00 2001 From: Martin Hochel Date: Thu, 22 Sep 2022 19:58:08 +0200 Subject: [PATCH 2/3] ci(github): setup triage bot --- .github/triage-bot.config.json | 21 +++++++++++++++++++++ .github/workflows/issues.yml | 15 ++++++--------- 2 files changed, 27 insertions(+), 9 deletions(-) create mode 100644 .github/triage-bot.config.json 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}); From a16631d20ea5daba593479f679be7cd942ecde13 Mon Sep 17 00:00:00 2001 From: Martin Hochel Date: Fri, 23 Sep 2022 11:18:32 +0200 Subject: [PATCH 3/3] chore: move octokit to single version policy, add @actions packages --- package.json | 2 ++ yarn.lock | 54 +++++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 55 insertions(+), 1 deletion(-) 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/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"