From 261cbab716f3f92cae09a4e5a8e0aba26da64145 Mon Sep 17 00:00:00 2001 From: Spencer Small <641091+ssmall@users.noreply.github.com> Date: Wed, 27 May 2020 15:18:45 -0700 Subject: [PATCH] Support de-duping issues (#65) * De-dupe against open issues when dedupe_issues = true * Update README * Fix compile errors * Add unit tests for issue.ts --- README.md | 1 + __tests__/issue.test.ts | 69 +++++++++++++++++++++++++++++++++++++++++ action.yml | 4 +++ dist/index.js | 36 ++++++++++++++++++--- src/issue.ts | 25 +++++++++++++++ src/main.ts | 28 ++++++++++++++--- 6 files changed, 154 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 53efba6..375c120 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,7 @@ If vulnerabilities are found by `npm audit`, Action triggered by push, schedule |issue_title|false|npm audit found vulnerabilities|Issue title| |token|true|N/A|GitHub Access Token.
${{ secrets.GITHUB_TOKEN }} is recommended.| |working_directory|false|N/A|The directory which contains package.json (since v1.4.0)| +|dedupe_issues|false|false|If 'true', action will not create a new issue when one is already open| ### Outputs diff --git a/__tests__/issue.test.ts b/__tests__/issue.test.ts index 81530d7..4e58042 100644 --- a/__tests__/issue.test.ts +++ b/__tests__/issue.test.ts @@ -42,3 +42,72 @@ describe('getIssueOption', () => { expect(issue.getIssueOption('hi')).toEqual(expected) }) }) + +describe('getExistingIssueNumber', () => { + const expectedTitle = 'expected title' + const expectedIssueNumber = 12345 + const repo = 'testRepo' + const owner = 'testOwner' + + beforeEach(() => { + process.env.INPUT_ISSUE_TITLE = expectedTitle + }) + + test('gets existing open issue', async () => { + const getIssues = jest.fn() + getIssues.mockResolvedValue({ + data: [ + { + title: expectedTitle, + number: expectedIssueNumber + } + ] + }) + const result = await issue.getExistingIssueNumber(getIssues, {repo, owner}) + + expect(getIssues).toHaveBeenCalledWith({ + repo, + owner, + state: 'open' + }) + + expect(result).toBe(expectedIssueNumber) + }) + + test('returns null when there is no open issue', async () => { + const getIssues = jest.fn() + getIssues.mockResolvedValue({data: []}) + + const result = await issue.getExistingIssueNumber(getIssues, {repo, owner}) + + expect(getIssues).toHaveBeenCalledWith({ + repo, + owner, + state: 'open' + }) + + expect(result).toBe(null) + }) + + test('returns null when no issues match the issue title', async () => { + const getIssues = jest.fn() + getIssues.mockResolvedValue({ + data: [ + { + title: 'some random other issue', + number: 54321 + } + ] + }) + + const result = await issue.getExistingIssueNumber(getIssues, {repo, owner}) + + expect(getIssues).toHaveBeenCalledWith({ + repo, + owner, + state: 'open' + }) + + expect(result).toBe(null) + }) +}) diff --git a/action.yml b/action.yml index fca26ba..09a1f8f 100644 --- a/action.yml +++ b/action.yml @@ -26,6 +26,10 @@ inputs: working_directory: description: 'The directory which contains package.json (since v1.4.0)' required: false + dedupe_issues: + description: 'Flag to de-dupe against open issues' + default: 'false' + required: false runs: using: 'node12' main: 'dist/index.js' diff --git a/dist/index.js b/dist/index.js index 984044c..59bb4e0 100644 --- a/dist/index.js +++ b/dist/index.js @@ -3338,8 +3338,17 @@ function run() { // remove control characters and create a code block const issueBody = audit.strippedStdout(); const option = issue.getIssueOption(issueBody); - const { data: createdIssue } = yield octokit.issues.create(Object.assign(Object.assign({}, github.context.repo), option)); - core.debug(`#${createdIssue.number}`); + const existingIssueNumber = core.getInput('dedupe_issues') === 'true' + ? yield issue.getExistingIssueNumber(octokit.issues.listForRepo, github.context.repo) + : null; + if (existingIssueNumber !== null) { + const { data: createdComment } = yield octokit.issues.createComment(Object.assign(Object.assign({}, github.context.repo), { issue_number: existingIssueNumber, body: option.body })); + core.debug(`comment ${createdComment.url}`); + } + else { + const { data: createdIssue } = yield octokit.issues.create(Object.assign(Object.assign({}, github.context.repo), option)); + core.debug(`#${createdIssue.number}`); + } core.setFailed('This repo has some vulnerabilities'); } } @@ -6952,8 +6961,17 @@ var __importStar = (this && this.__importStar) || function (mod) { __setModuleDefault(result, mod); return result; }; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; Object.defineProperty(exports, "__esModule", { value: true }); -exports.getIssueOption = void 0; +exports.getExistingIssueNumber = exports.getIssueOption = void 0; const core = __importStar(__webpack_require__(470)); function getIssueOption(body) { let assignees; @@ -6972,6 +6990,16 @@ function getIssueOption(body) { }; } exports.getIssueOption = getIssueOption; +function getExistingIssueNumber(getIssues, repo) { + return __awaiter(this, void 0, void 0, function* () { + const { data: issues } = yield getIssues(Object.assign(Object.assign({}, repo), { state: 'open' })); + const iss = issues + .filter(i => i.title === core.getInput('issue_title')) + .shift(); + return iss === undefined ? null : iss.number; + }); +} +exports.getExistingIssueNumber = getExistingIssueNumber; /***/ }), @@ -30373,7 +30401,7 @@ exports.requestLog = requestLog; /***/ 919: /***/ (function(module) { -module.exports = {"_from":"@octokit/rest@^16.43.1","_id":"@octokit/rest@16.43.1","_inBundle":false,"_integrity":"sha512-gfFKwRT/wFxq5qlNjnW2dh+qh74XgTQ2B179UX5K1HYCluioWj8Ndbgqw2PVqa1NnVJkGHp2ovMpVn/DImlmkw==","_location":"/@actions/github/@octokit/rest","_phantomChildren":{},"_requested":{"type":"range","registry":true,"raw":"@octokit/rest@^16.43.1","name":"@octokit/rest","escapedName":"@octokit%2frest","scope":"@octokit","rawSpec":"^16.43.1","saveSpec":null,"fetchSpec":"^16.43.1"},"_requiredBy":["/@actions/github"],"_resolved":"https://registry.npmjs.org/@octokit/rest/-/rest-16.43.1.tgz","_shasum":"3b11e7d1b1ac2bbeeb23b08a17df0b20947eda6b","_spec":"@octokit/rest@^16.43.1","_where":"/Users/naoki/go/src/github.com/oke-py/npm-audit-action/node_modules/@actions/github","author":{"name":"Gregor Martynus","url":"https://github.com/gr2m"},"bugs":{"url":"https://github.com/octokit/rest.js/issues"},"bundleDependencies":false,"bundlesize":[{"path":"./dist/octokit-rest.min.js.gz","maxSize":"33 kB"}],"contributors":[{"name":"Mike de Boer","email":"info@mikedeboer.nl"},{"name":"Fabian Jakobs","email":"fabian@c9.io"},{"name":"Joe Gallo","email":"joe@brassafrax.com"},{"name":"Gregor Martynus","url":"https://github.com/gr2m"}],"dependencies":{"@octokit/auth-token":"^2.4.0","@octokit/plugin-paginate-rest":"^1.1.1","@octokit/plugin-request-log":"^1.0.0","@octokit/plugin-rest-endpoint-methods":"2.4.0","@octokit/request":"^5.2.0","@octokit/request-error":"^1.0.2","atob-lite":"^2.0.0","before-after-hook":"^2.0.0","btoa-lite":"^1.0.0","deprecation":"^2.0.0","lodash.get":"^4.4.2","lodash.set":"^4.3.2","lodash.uniq":"^4.5.0","octokit-pagination-methods":"^1.1.0","once":"^1.4.0","universal-user-agent":"^4.0.0"},"deprecated":false,"description":"GitHub REST API client for Node.js","devDependencies":{"@gimenete/type-writer":"^0.1.3","@octokit/auth":"^1.1.1","@octokit/fixtures-server":"^5.0.6","@octokit/graphql":"^4.2.0","@types/node":"^13.1.0","bundlesize":"^0.18.0","chai":"^4.1.2","compression-webpack-plugin":"^3.1.0","cypress":"^3.0.0","glob":"^7.1.2","http-proxy-agent":"^4.0.0","lodash.camelcase":"^4.3.0","lodash.merge":"^4.6.1","lodash.upperfirst":"^4.3.1","lolex":"^5.1.2","mkdirp":"^1.0.0","mocha":"^7.0.1","mustache":"^4.0.0","nock":"^11.3.3","npm-run-all":"^4.1.2","nyc":"^15.0.0","prettier":"^1.14.2","proxy":"^1.0.0","semantic-release":"^17.0.0","sinon":"^8.0.0","sinon-chai":"^3.0.0","sort-keys":"^4.0.0","string-to-arraybuffer":"^1.0.0","string-to-jsdoc-comment":"^1.0.0","typescript":"^3.3.1","webpack":"^4.0.0","webpack-bundle-analyzer":"^3.0.0","webpack-cli":"^3.0.0"},"files":["index.js","index.d.ts","lib","plugins"],"homepage":"https://github.com/octokit/rest.js#readme","keywords":["octokit","github","rest","api-client"],"license":"MIT","name":"@octokit/rest","nyc":{"ignore":["test"]},"publishConfig":{"access":"public"},"release":{"publish":["@semantic-release/npm",{"path":"@semantic-release/github","assets":["dist/*","!dist/*.map.gz"]}]},"repository":{"type":"git","url":"git+https://github.com/octokit/rest.js.git"},"scripts":{"build":"npm-run-all build:*","build:browser":"npm-run-all build:browser:*","build:browser:development":"webpack --mode development --entry . --output-library=Octokit --output=./dist/octokit-rest.js --profile --json > dist/bundle-stats.json","build:browser:production":"webpack --mode production --entry . --plugin=compression-webpack-plugin --output-library=Octokit --output-path=./dist --output-filename=octokit-rest.min.js --devtool source-map","build:ts":"npm run -s update-endpoints:typescript","coverage":"nyc report --reporter=html && open coverage/index.html","generate-bundle-report":"webpack-bundle-analyzer dist/bundle-stats.json --mode=static --no-open --report dist/bundle-report.html","lint":"prettier --check '{lib,plugins,scripts,test}/**/*.{js,json,ts}' 'docs/*.{js,json}' 'docs/src/**/*' index.js README.md package.json","lint:fix":"prettier --write '{lib,plugins,scripts,test}/**/*.{js,json,ts}' 'docs/*.{js,json}' 'docs/src/**/*' index.js README.md package.json","postvalidate:ts":"tsc --noEmit --target es6 test/typescript-validate.ts","prebuild:browser":"mkdirp dist/","pretest":"npm run -s lint","prevalidate:ts":"npm run -s build:ts","start-fixtures-server":"octokit-fixtures-server","test":"nyc mocha test/mocha-node-setup.js \"test/*/**/*-test.js\"","test:browser":"cypress run --browser chrome","update-endpoints":"npm-run-all update-endpoints:*","update-endpoints:fetch-json":"node scripts/update-endpoints/fetch-json","update-endpoints:typescript":"node scripts/update-endpoints/typescript","validate:ts":"tsc --target es6 --noImplicitAny index.d.ts"},"types":"index.d.ts","version":"16.43.1"}; +module.exports = {"name":"@octokit/rest","version":"16.43.1","publishConfig":{"access":"public"},"description":"GitHub REST API client for Node.js","keywords":["octokit","github","rest","api-client"],"author":"Gregor Martynus (https://github.com/gr2m)","contributors":[{"name":"Mike de Boer","email":"info@mikedeboer.nl"},{"name":"Fabian Jakobs","email":"fabian@c9.io"},{"name":"Joe Gallo","email":"joe@brassafrax.com"},{"name":"Gregor Martynus","url":"https://github.com/gr2m"}],"repository":"https://github.com/octokit/rest.js","dependencies":{"@octokit/auth-token":"^2.4.0","@octokit/plugin-paginate-rest":"^1.1.1","@octokit/plugin-request-log":"^1.0.0","@octokit/plugin-rest-endpoint-methods":"2.4.0","@octokit/request":"^5.2.0","@octokit/request-error":"^1.0.2","atob-lite":"^2.0.0","before-after-hook":"^2.0.0","btoa-lite":"^1.0.0","deprecation":"^2.0.0","lodash.get":"^4.4.2","lodash.set":"^4.3.2","lodash.uniq":"^4.5.0","octokit-pagination-methods":"^1.1.0","once":"^1.4.0","universal-user-agent":"^4.0.0"},"devDependencies":{"@gimenete/type-writer":"^0.1.3","@octokit/auth":"^1.1.1","@octokit/fixtures-server":"^5.0.6","@octokit/graphql":"^4.2.0","@types/node":"^13.1.0","bundlesize":"^0.18.0","chai":"^4.1.2","compression-webpack-plugin":"^3.1.0","cypress":"^3.0.0","glob":"^7.1.2","http-proxy-agent":"^4.0.0","lodash.camelcase":"^4.3.0","lodash.merge":"^4.6.1","lodash.upperfirst":"^4.3.1","lolex":"^5.1.2","mkdirp":"^1.0.0","mocha":"^7.0.1","mustache":"^4.0.0","nock":"^11.3.3","npm-run-all":"^4.1.2","nyc":"^15.0.0","prettier":"^1.14.2","proxy":"^1.0.0","semantic-release":"^17.0.0","sinon":"^8.0.0","sinon-chai":"^3.0.0","sort-keys":"^4.0.0","string-to-arraybuffer":"^1.0.0","string-to-jsdoc-comment":"^1.0.0","typescript":"^3.3.1","webpack":"^4.0.0","webpack-bundle-analyzer":"^3.0.0","webpack-cli":"^3.0.0"},"types":"index.d.ts","scripts":{"coverage":"nyc report --reporter=html && open coverage/index.html","lint":"prettier --check '{lib,plugins,scripts,test}/**/*.{js,json,ts}' 'docs/*.{js,json}' 'docs/src/**/*' index.js README.md package.json","lint:fix":"prettier --write '{lib,plugins,scripts,test}/**/*.{js,json,ts}' 'docs/*.{js,json}' 'docs/src/**/*' index.js README.md package.json","pretest":"npm run -s lint","test":"nyc mocha test/mocha-node-setup.js \"test/*/**/*-test.js\"","test:browser":"cypress run --browser chrome","build":"npm-run-all build:*","build:ts":"npm run -s update-endpoints:typescript","prebuild:browser":"mkdirp dist/","build:browser":"npm-run-all build:browser:*","build:browser:development":"webpack --mode development --entry . --output-library=Octokit --output=./dist/octokit-rest.js --profile --json > dist/bundle-stats.json","build:browser:production":"webpack --mode production --entry . --plugin=compression-webpack-plugin --output-library=Octokit --output-path=./dist --output-filename=octokit-rest.min.js --devtool source-map","generate-bundle-report":"webpack-bundle-analyzer dist/bundle-stats.json --mode=static --no-open --report dist/bundle-report.html","update-endpoints":"npm-run-all update-endpoints:*","update-endpoints:fetch-json":"node scripts/update-endpoints/fetch-json","update-endpoints:typescript":"node scripts/update-endpoints/typescript","prevalidate:ts":"npm run -s build:ts","validate:ts":"tsc --target es6 --noImplicitAny index.d.ts","postvalidate:ts":"tsc --noEmit --target es6 test/typescript-validate.ts","start-fixtures-server":"octokit-fixtures-server"},"license":"MIT","files":["index.js","index.d.ts","lib","plugins"],"nyc":{"ignore":["test"]},"release":{"publish":["@semantic-release/npm",{"path":"@semantic-release/github","assets":["dist/*","!dist/*.map.gz"]}]},"bundlesize":[{"path":"./dist/octokit-rest.min.js.gz","maxSize":"33 kB"}],"_resolved":"https://registry.npmjs.org/@octokit/rest/-/rest-16.43.1.tgz","_integrity":"sha512-gfFKwRT/wFxq5qlNjnW2dh+qh74XgTQ2B179UX5K1HYCluioWj8Ndbgqw2PVqa1NnVJkGHp2ovMpVn/DImlmkw==","_from":"@octokit/rest@16.43.1"}; /***/ }), diff --git a/src/issue.ts b/src/issue.ts index aafd657..88c4e27 100644 --- a/src/issue.ts +++ b/src/issue.ts @@ -19,3 +19,28 @@ export function getIssueOption(body: string): IssueOption { labels } } + +export type GetIssuesFunc = (options: { + owner: string + repo: string + state: 'open' | 'closed' | 'all' | undefined +}) => Promise<{data: {title: string; number: number}[]}> + +export async function getExistingIssueNumber( + getIssues: GetIssuesFunc, + repo: { + owner: string + repo: string + } +): Promise { + const {data: issues} = await getIssues({ + ...repo, + state: 'open' + }) + + const iss = issues + .filter(i => i.title === core.getInput('issue_title')) + .shift() + + return iss === undefined ? null : iss.number +} diff --git a/src/main.ts b/src/main.ts index c6642ba..6c44c0b 100644 --- a/src/main.ts +++ b/src/main.ts @@ -55,11 +55,29 @@ export async function run(): Promise { // remove control characters and create a code block const issueBody = audit.strippedStdout() const option: IssueOption = issue.getIssueOption(issueBody) - const {data: createdIssue} = await octokit.issues.create({ - ...github.context.repo, - ...option - }) - core.debug(`#${createdIssue.number}`) + + const existingIssueNumber = + core.getInput('dedupe_issues') === 'true' + ? await issue.getExistingIssueNumber( + octokit.issues.listForRepo, + github.context.repo + ) + : null + + if (existingIssueNumber !== null) { + const {data: createdComment} = await octokit.issues.createComment({ + ...github.context.repo, + issue_number: existingIssueNumber, // eslint-disable-line @typescript-eslint/camelcase + body: option.body + }) + core.debug(`comment ${createdComment.url}`) + } else { + const {data: createdIssue} = await octokit.issues.create({ + ...github.context.repo, + ...option + }) + core.debug(`#${createdIssue.number}`) + } core.setFailed('This repo has some vulnerabilities') } }