From 2befe4b23218007df3ed1446d466e9747131dd1a Mon Sep 17 00:00:00 2001 From: Mary Marchini Date: Sat, 1 Aug 2020 17:22:38 -0700 Subject: [PATCH] feat: ping teams based on which files were changed Using .github/CODEOWNERS, `github-bot` will ping the appropriate teams based on which files were changed in a Pull Request. This feature is inteded to work around GitHub's limitation which prevents teams without explicit write access from being added as reviewers (thus preventing the vast majority of teams in the org from being used on GitHub's CODEOWNERS feature). Ref: https://github.com/nodejs/node/issues/33984 --- lib/logger.js | 2 +- lib/node-owners.js | 31 ++ lib/node-repo.js | 109 +++++ package-lock.json | 385 ++++++++++++------ package.json | 3 + scripts/node-ping-owners.js | 27 ++ test/_fixtures/CODEOWNERS | 8 + test/_fixtures/get-repository.json | 132 ++++++ .../pull-request-create-comment.json | 32 ++ test/read-fixture.js | 2 +- test/unit/node-owners.test.js | 89 ++++ test/unit/node-repo-owners.test.js | 186 +++++++++ 12 files changed, 869 insertions(+), 137 deletions(-) create mode 100644 lib/node-owners.js create mode 100644 scripts/node-ping-owners.js create mode 100644 test/_fixtures/CODEOWNERS create mode 100644 test/_fixtures/get-repository.json create mode 100644 test/_fixtures/pull-request-create-comment.json create mode 100644 test/unit/node-owners.test.js create mode 100644 test/unit/node-repo-owners.test.js diff --git a/lib/logger.js b/lib/logger.js index d52657ba..e9c2a984 100644 --- a/lib/logger.js +++ b/lib/logger.js @@ -4,7 +4,7 @@ const path = require('path') const bunyan = require('bunyan') const isRunningTests = process.env.npm_lifecycle_event === 'test' -const stdoutLevel = isRunningTests ? 'FATAL' : 'INFO' +const stdoutLevel = isRunningTests ? 'FATAL' : process.env.LOG_LEVEL || 'INFO' const daysToKeepLogs = process.env.KEEP_LOGS || 10 const logsDir = process.env.LOGS_DIR || '' diff --git a/lib/node-owners.js b/lib/node-owners.js new file mode 100644 index 00000000..e4184ebc --- /dev/null +++ b/lib/node-owners.js @@ -0,0 +1,31 @@ +'use static' + +const { parse } = require('codeowners-utils') +// NOTE(mmarchini): `codeowners-utils` doesn't respect ./ prefix, +// so we use micromatch +const micromatch = require('micromatch') + +class Owners { + constructor (ownersDefinitions) { + this._ownersDefinitions = ownersDefinitions + } + + static fromFile (content) { + return new Owners(parse(content)) + } + + getOwnersForPaths (paths) { + let ownersForPath = [] + for (const { pattern, owners } of this._ownersDefinitions) { + if (micromatch(paths, pattern).length > 0) { + ownersForPath = ownersForPath.concat(owners) + } + } + // Remove duplicates before returning + return ownersForPath.filter((v, i) => ownersForPath.indexOf(v) === i).sort() + } +} + +module.exports = { + Owners +} diff --git a/lib/node-repo.js b/lib/node-repo.js index 1c3ee860..4a6e67df 100644 --- a/lib/node-repo.js +++ b/lib/node-repo.js @@ -2,9 +2,13 @@ const LRU = require('lru-cache') const retry = require('async').retry +const Aigle = require('aigle') +const request = require('request') const githubClient = require('./github-client') +const { createPrComment } = require('./github-comment') const resolveLabels = require('./node-labels').resolveLabels +const { Owners } = require('./node-owners') const existingLabelsCache = new LRU({ max: 1, maxAge: 1000 * 60 * 60 }) const fiveSeconds = 5 * 1000 @@ -185,10 +189,115 @@ function stringsInCommon (arr1, arr2) { return arr1.filter((str) => loweredArr2.indexOf(str.toLowerCase()) !== -1) } +async function deferredResolveOwnersThenPingPr (options) { + const timeoutMillis = (options.timeoutInSec || 0) * 1000 + await sleep(timeoutMillis) + return resolveOwnersThenPingPr(options) +} + +function getCodeOwnersUrl (owner, repo, defaultBranch) { + const base = 'raw.githubusercontent.com' + const filepath = '.github/CODEOWNERS' + return `https://${base}/${owner}/${repo}/${defaultBranch}/${filepath}` +} + +async function getFiles ({ owner, repo, number, logger }) { + try { + const response = await githubClient.pullRequests.getFiles({ + owner, + repo, + number + }) + return response.data.map(({ filename }) => filename) + } catch (err) { + logger.error(err, 'Error retrieving files from GitHub') + throw err + } +} + +async function getDefaultBranch ({ owner, repo, logger }) { + try { + const data = (await githubClient.repos.get({ owner, repo })).data || { } + + if (!data['default_branch']) { + logger.error(null, 'Couldn\' determine default branch') + throw new Error('unknown default branch') + } + + return data.default_branch + } catch (err) { + logger.error(err, 'Error retrieving repository data') + throw err + } +} + +function getCodeOwnersFile (url, { logger }) { + return new Promise((resolve, reject) => { + request(url, (err, res, body) => { + if (err || res.statusCode !== 200) { + logger.error(err, 'Error retrieving OWNERS') + return reject(err) + } + return resolve(body) + }) + }) +} + +async function resolveOwnersThenPingPr (options) { + const { owner, repo } = options + const times = options.retries || 5 + const interval = options.retryInterval || fiveSeconds + const retry = fn => Aigle.retry({ times, interval }, fn) + + options.logger.debug('getting file paths') + options.number = options.prId + const filepathsChanged = await retry(() => getFiles(options)) + + options.logger.debug('getting default branch') + const defaultBranch = await retry(() => getDefaultBranch(options)) + + const url = getCodeOwnersUrl(owner, repo, defaultBranch) + options.logger.debug(`Fetching OWNERS on ${url}`) + + const file = await retry(() => getCodeOwnersFile(url, options)) + + options.logger.debug('parsing codeowners file') + const owners = Owners.fromFile(file) + const selectedOwners = owners.getOwnersForPaths(filepathsChanged) + + options.logger.debug('pinging codeowners file') + if (selectedOwners.length > 0) { + await pingOwners(options, selectedOwners) + } +} + +function getCommentForOwners (owners) { + return `Review requested:\n\n${owners.map(i => `- [ ] ${i}`).join('\n')}` +} + +async function pingOwners (options, owners) { + try { + await createPrComment({ + owner: options.owner, + repo: options.repo, + number: options.prId, + logger: options.logger + }, getCommentForOwners(owners)) + } catch (err) { + options.logger.error(err, 'Error while pinging owners') + throw err + } + options.logger.debug('Pinged owners: ' + owners) +} + exports.getBotPrLabels = getBotPrLabels exports.removeLabelFromPR = removeLabelFromPR exports.fetchExistingThenUpdatePr = fetchExistingThenUpdatePr exports.resolveLabelsThenUpdatePr = deferredResolveLabelsThenUpdatePr +exports.resolveOwnersThenPingPr = deferredResolveOwnersThenPingPr // exposed for testability exports._fetchExistingLabels = fetchExistingLabels +exports._testExports = { + pingOwners, getCodeOwnersFile, getCodeOwnersUrl, getDefaultBranch, getFiles, getCommentForOwners +} diff --git a/package-lock.json b/package-lock.json index 68c91dc3..c2f2909b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -150,21 +150,6 @@ "to-fast-properties": "^2.0.0" } }, - "@sindresorhus/is": { - "version": "0.14.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.14.0.tgz", - "integrity": "sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ==", - "dev": true - }, - "@szmarczak/http-timer": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-1.1.2.tgz", - "integrity": "sha512-XIB2XbzHTN6ieIjfIMV9hlVcfPU26s2vafYWQcZHWXHOxiaRZYEDKEwdl129Zyg50+foYV2jCgtrqSA6qNuNSA==", - "dev": true, - "requires": { - "defer-to-connect": "^1.0.1" - } - }, "@types/body-parser": { "version": "1.17.0", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.17.0.tgz", @@ -280,6 +265,19 @@ "es6-promisify": "^5.0.0" } }, + "aigle": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/aigle/-/aigle-1.14.1.tgz", + "integrity": "sha512-bCmQ65CEebspmpbWFs6ab3S27TNyVH1b5MledX8KoiGxUhsJmPUUGpaoSijhwawNnq5Lt8jbcq7Z7gUAD0nuTw==", + "requires": { + "aigle-core": "^1.0.0" + } + }, + "aigle-core": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/aigle-core/-/aigle-core-1.0.0.tgz", + "integrity": "sha1-QGbg+aXGCZbYN37RlqIfoXtiUKs=" + }, "ajv": { "version": "6.10.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.0.tgz", @@ -680,7 +678,6 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dev": true, "requires": { "fill-range": "^7.0.1" } @@ -723,38 +720,6 @@ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=" }, - "cacheable-request": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-6.1.0.tgz", - "integrity": "sha512-Oj3cAGPCqOZX7Rz64Uny2GYAZNliQSqfbePrgAQ1wKAihYmCUnraBtJtKcGR4xz7wF+LoJC+ssFZvv5BgF9Igg==", - "dev": true, - "requires": { - "clone-response": "^1.0.2", - "get-stream": "^5.1.0", - "http-cache-semantics": "^4.0.0", - "keyv": "^3.0.0", - "lowercase-keys": "^2.0.0", - "normalize-url": "^4.1.0", - "responselike": "^1.0.2" - }, - "dependencies": { - "get-stream": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.1.0.tgz", - "integrity": "sha512-EXr1FOzrzTfGeL0gQdeFEvOMm2mzMOglyiOXSTpPC+iAjAKftbr3jpCMWynogwYnM+eSj9sHGc6wjIcDvYiygw==", - "dev": true, - "requires": { - "pump": "^3.0.0" - } - }, - "lowercase-keys": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", - "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", - "dev": true - } - } - }, "caching-transform": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/caching-transform/-/caching-transform-3.0.2.tgz", @@ -909,6 +874,67 @@ "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", "dev": true }, + "codeowners-utils": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/codeowners-utils/-/codeowners-utils-1.0.2.tgz", + "integrity": "sha512-4oLRCymV7azxGHMpM3F297D651VdwZa21hVfFCn/cOd8Fq8tFrpfpyRpSBQkaZCyFPkfOhEld9xceCF7btyiug==", + "requires": { + "cross-spawn": "^7.0.2", + "find-up": "^4.1.0", + "ignore": "^5.1.4", + "locate-path": "^5.0.0" + }, + "dependencies": { + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "ignore": { + "version": "5.1.8", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.8.tgz", + "integrity": "sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw==" + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "requires": { + "p-locate": "^4.1.0" + } + }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "requires": { + "p-limit": "^2.2.0" + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==" + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==" + } + } + }, "color-convert": { "version": "1.9.2", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.2.tgz", @@ -1079,6 +1105,44 @@ } } }, + "cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "dependencies": { + "path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==" + }, + "shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==" + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "requires": { + "isexe": "^2.0.0" + } + } + } + }, "crypto-random-string": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz", @@ -1113,15 +1177,6 @@ "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", "dev": true }, - "decompress-response": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-3.3.0.tgz", - "integrity": "sha1-gKTdMjdIOEv6JICDYirt7Jgq3/M=", - "dev": true, - "requires": { - "mimic-response": "^1.0.0" - } - }, "deep-extend": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", @@ -1143,12 +1198,6 @@ "strip-bom": "^3.0.0" } }, - "defer-to-connect": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-1.1.3.tgz", - "integrity": "sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ==", - "dev": true - }, "define-properties": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", @@ -1858,7 +1907,6 @@ "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, "requires": { "to-regex-range": "^5.0.1" } @@ -2156,15 +2204,6 @@ "integrity": "sha512-jp4tHawyV7+fkkSKyvjuLZswblUtz+SQKzSWnBbii16BuZksJlU1wuBYXY75r+duh/llF1ur6oNwi+2ZzjKZ7g==", "dev": true }, - "get-stream": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", - "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", - "dev": true, - "requires": { - "pump": "^3.0.0" - } - }, "getpass": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", @@ -2244,25 +2283,6 @@ "integrity": "sha512-WHq43gS+6ufNOEqlrDBxVEbb8ntfXrfAUU2ZOpCxrBdGKW3gyv8mCxAfIBD0DroPKGrJ2eSsXsLtY9MPntsyTw==", "dev": true }, - "got": { - "version": "9.6.0", - "resolved": "https://registry.npmjs.org/got/-/got-9.6.0.tgz", - "integrity": "sha512-R7eWptXuGYxwijs0eV+v3o6+XH1IqVK8dJOEecQfTmkncw9AV4dcw/Dhxi8MdlqPthxxpZyizMzyg8RTmEsG+Q==", - "dev": true, - "requires": { - "@sindresorhus/is": "^0.14.0", - "@szmarczak/http-timer": "^1.1.2", - "cacheable-request": "^6.0.0", - "decompress-response": "^3.3.0", - "duplexer3": "^0.1.4", - "get-stream": "^4.1.0", - "lowercase-keys": "^1.0.1", - "mimic-response": "^1.0.1", - "p-cancelable": "^1.0.0", - "to-readable-stream": "^1.0.0", - "url-parse-lax": "^3.0.0" - } - }, "graceful-fs": { "version": "4.1.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", @@ -2553,8 +2573,7 @@ "is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==" }, "is-obj": { "version": "2.0.0", @@ -2629,8 +2648,7 @@ "isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", - "dev": true + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" }, "isstream": { "version": "0.1.2", @@ -2896,12 +2914,6 @@ "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", "dev": true }, - "json-buffer": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.0.tgz", - "integrity": "sha1-Wx85evx11ne96Lz8Dkfh+aPZqJg=", - "dev": true - }, "json-parse-better-errors": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", @@ -2949,15 +2961,6 @@ "array-includes": "^3.0.3" } }, - "keyv": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-3.1.0.tgz", - "integrity": "sha512-9ykJ/46SN/9KPM/sichzQ7OvXyGDYKGTaDlKMGCAlg2UK8KRy4jb0d8sFc+0Tt0YYnThq8X2RZgCg74RPxgcVA==", - "dev": true, - "requires": { - "json-buffer": "3.0.0" - } - }, "latest-version": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-5.1.0.tgz", @@ -3045,12 +3048,6 @@ "js-tokens": "^3.0.0 || ^4.0.0" } }, - "lowercase-keys": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz", - "integrity": "sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==", - "dev": true - }, "lru-cache": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.3.tgz", @@ -3107,6 +3104,15 @@ "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" }, + "micromatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz", + "integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==", + "requires": { + "braces": "^3.0.1", + "picomatch": "^2.0.5" + } + }, "mime": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/mime/-/mime-1.4.1.tgz", @@ -3606,12 +3612,6 @@ "own-or": "^1.0.0" } }, - "p-cancelable": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-1.1.0.tgz", - "integrity": "sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw==", - "dev": true - }, "p-limit": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", @@ -3668,6 +3668,132 @@ "semver": "^6.2.0" }, "dependencies": { + "@sindresorhus/is": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.14.0.tgz", + "integrity": "sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ==", + "dev": true + }, + "@szmarczak/http-timer": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-1.1.2.tgz", + "integrity": "sha512-XIB2XbzHTN6ieIjfIMV9hlVcfPU26s2vafYWQcZHWXHOxiaRZYEDKEwdl129Zyg50+foYV2jCgtrqSA6qNuNSA==", + "dev": true, + "requires": { + "defer-to-connect": "^1.0.1" + } + }, + "cacheable-request": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-6.1.0.tgz", + "integrity": "sha512-Oj3cAGPCqOZX7Rz64Uny2GYAZNliQSqfbePrgAQ1wKAihYmCUnraBtJtKcGR4xz7wF+LoJC+ssFZvv5BgF9Igg==", + "dev": true, + "requires": { + "clone-response": "^1.0.2", + "get-stream": "^5.1.0", + "http-cache-semantics": "^4.0.0", + "keyv": "^3.0.0", + "lowercase-keys": "^2.0.0", + "normalize-url": "^4.1.0", + "responselike": "^1.0.2" + }, + "dependencies": { + "get-stream": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.1.0.tgz", + "integrity": "sha512-EXr1FOzrzTfGeL0gQdeFEvOMm2mzMOglyiOXSTpPC+iAjAKftbr3jpCMWynogwYnM+eSj9sHGc6wjIcDvYiygw==", + "dev": true, + "requires": { + "pump": "^3.0.0" + } + }, + "lowercase-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", + "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", + "dev": true + } + } + }, + "decompress-response": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-3.3.0.tgz", + "integrity": "sha1-gKTdMjdIOEv6JICDYirt7Jgq3/M=", + "dev": true, + "requires": { + "mimic-response": "^1.0.0" + } + }, + "defer-to-connect": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-1.1.3.tgz", + "integrity": "sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ==", + "dev": true + }, + "get-stream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", + "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "dev": true, + "requires": { + "pump": "^3.0.0" + } + }, + "got": { + "version": "9.6.0", + "resolved": "https://registry.npmjs.org/got/-/got-9.6.0.tgz", + "integrity": "sha512-R7eWptXuGYxwijs0eV+v3o6+XH1IqVK8dJOEecQfTmkncw9AV4dcw/Dhxi8MdlqPthxxpZyizMzyg8RTmEsG+Q==", + "dev": true, + "requires": { + "@sindresorhus/is": "^0.14.0", + "@szmarczak/http-timer": "^1.1.2", + "cacheable-request": "^6.0.0", + "decompress-response": "^3.3.0", + "duplexer3": "^0.1.4", + "get-stream": "^4.1.0", + "lowercase-keys": "^1.0.1", + "mimic-response": "^1.0.1", + "p-cancelable": "^1.0.0", + "to-readable-stream": "^1.0.0", + "url-parse-lax": "^3.0.0" + } + }, + "json-buffer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.0.tgz", + "integrity": "sha1-Wx85evx11ne96Lz8Dkfh+aPZqJg=", + "dev": true + }, + "keyv": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-3.1.0.tgz", + "integrity": "sha512-9ykJ/46SN/9KPM/sichzQ7OvXyGDYKGTaDlKMGCAlg2UK8KRy4jb0d8sFc+0Tt0YYnThq8X2RZgCg74RPxgcVA==", + "dev": true, + "requires": { + "json-buffer": "3.0.0" + } + }, + "lowercase-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz", + "integrity": "sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==", + "dev": true + }, + "p-cancelable": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-1.1.0.tgz", + "integrity": "sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw==", + "dev": true + }, + "responselike": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-1.0.2.tgz", + "integrity": "sha1-kYcg7ztjHFZCvgaPFa3lpG9Loec=", + "dev": true, + "requires": { + "lowercase-keys": "^1.0.0" + } + }, "semver": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", @@ -3749,8 +3875,7 @@ "picomatch": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.1.tgz", - "integrity": "sha512-ISBaA8xQNmwELC7eOjqFKMESB2VIqt4PPDD0nsS95b/9dZXvVKOlz9keMSnoGGKcOHXfTvDD6WMaRoSc9UuhRA==", - "dev": true + "integrity": "sha512-ISBaA8xQNmwELC7eOjqFKMESB2VIqt4PPDD0nsS95b/9dZXvVKOlz9keMSnoGGKcOHXfTvDD6WMaRoSc9UuhRA==" }, "pify": { "version": "3.0.0", @@ -4165,15 +4290,6 @@ "integrity": "sha1-Jsv+k10a7uq7Kbw/5a6wHpPUQiY=", "dev": true }, - "responselike": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/responselike/-/responselike-1.0.2.tgz", - "integrity": "sha1-kYcg7ztjHFZCvgaPFa3lpG9Loec=", - "dev": true, - "requires": { - "lowercase-keys": "^1.0.0" - } - }, "restore-cursor": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", @@ -6004,7 +6120,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, "requires": { "is-number": "^7.0.0" } diff --git a/package.json b/package.json index 5ced5e94..5af0ef88 100644 --- a/package.json +++ b/package.json @@ -13,11 +13,13 @@ "private": true, "license": "MIT", "dependencies": { + "aigle": "^1.14.1", "async": "2.1.5", "basic-auth": "^1.0.4", "body-parser": "^1.15.0", "bunyan": "^1.8.1", "bunyan-middleware": "0.8.0", + "codeowners-utils": "^1.0.2", "debug": "^2.2.0", "dotenv": "^2.0.0", "events-async": "^1.2.1", @@ -25,6 +27,7 @@ "github": "^13.1.1", "glob": "^7.0.3", "lru-cache": "^4.0.1", + "micromatch": "^4.0.2", "request": "^2.88.0" }, "devDependencies": { diff --git a/scripts/node-ping-owners.js b/scripts/node-ping-owners.js new file mode 100644 index 00000000..faad94c3 --- /dev/null +++ b/scripts/node-ping-owners.js @@ -0,0 +1,27 @@ +'use strict' + +const debug = require('debug')('node_ping_owners') + +const nodeRepo = require('../lib/node-repo') + +module.exports = function (app, events) { + events.on('pull_request.opened', handlePrCreated) +} + +function handlePrCreated (event, owner, repo) { + const prId = event.number + const logger = event.logger + const baseBranch = event.pull_request.base.ref + + debug(`/${owner}/${repo}/pull/${prId} opened`) + nodeRepo.resolveOwnersThenPingPr({ + owner, + repo, + prId, + logger, + baseBranch, + timeoutInSec: 2 + }).catch(err => { + event.logger.error(err, 'owners ping failed') + }) +} diff --git a/test/_fixtures/CODEOWNERS b/test/_fixtures/CODEOWNERS new file mode 100644 index 00000000..14ce6e74 --- /dev/null +++ b/test/_fixtures/CODEOWNERS @@ -0,0 +1,8 @@ +./file1 @nodejs/test1 +./file2 @nodejs/test2 +./file3 @nodejs/test1 @nodejs/test2 +./file4 @nodejs/test1 +./file5 @nodejs/test3 +./folder1/* @nodejs/test3 +./folder2/file1.* @nodejs/test4 @nodejs/test5 +./lib/timers.js @nodejs/team @nodejs/team2 diff --git a/test/_fixtures/get-repository.json b/test/_fixtures/get-repository.json new file mode 100644 index 00000000..1d64f028 --- /dev/null +++ b/test/_fixtures/get-repository.json @@ -0,0 +1,132 @@ +{ + "id": 108083039, + "node_id": "MDEwOlJlcG9zaXRvcnkxMDgwODMwMzk=", + "name": "node-auto-test", + "full_name": "nodejs/node-auto-test", + "private": false, + "owner": { + "login": "nodejs", + "id": 9950313, + "node_id": "MDEyOk9yZ2FuaXphdGlvbjk5NTAzMTM=", + "avatar_url": "https://avatars3.githubusercontent.com/u/9950313?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/nodejs", + "html_url": "https://github.com/nodejs", + "followers_url": "https://api.github.com/users/nodejs/followers", + "following_url": "https://api.github.com/users/nodejs/following{/other_user}", + "gists_url": "https://api.github.com/users/nodejs/gists{/gist_id}", + "starred_url": "https://api.github.com/users/nodejs/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/nodejs/subscriptions", + "organizations_url": "https://api.github.com/users/nodejs/orgs", + "repos_url": "https://api.github.com/users/nodejs/repos", + "events_url": "https://api.github.com/users/nodejs/events{/privacy}", + "received_events_url": "https://api.github.com/users/nodejs/received_events", + "type": "Organization", + "site_admin": false + }, + "html_url": "https://github.com/nodejs/node-auto-test", + "description": "Node.js clone for testing automation tools", + "fork": false, + "url": "https://api.github.com/repos/nodejs/node-auto-test", + "forks_url": "https://api.github.com/repos/nodejs/node-auto-test/forks", + "keys_url": "https://api.github.com/repos/nodejs/node-auto-test/keys{/key_id}", + "collaborators_url": "https://api.github.com/repos/nodejs/node-auto-test/collaborators{/collaborator}", + "teams_url": "https://api.github.com/repos/nodejs/node-auto-test/teams", + "hooks_url": "https://api.github.com/repos/nodejs/node-auto-test/hooks", + "issue_events_url": "https://api.github.com/repos/nodejs/node-auto-test/issues/events{/number}", + "events_url": "https://api.github.com/repos/nodejs/node-auto-test/events", + "assignees_url": "https://api.github.com/repos/nodejs/node-auto-test/assignees{/user}", + "branches_url": "https://api.github.com/repos/nodejs/node-auto-test/branches{/branch}", + "tags_url": "https://api.github.com/repos/nodejs/node-auto-test/tags", + "blobs_url": "https://api.github.com/repos/nodejs/node-auto-test/git/blobs{/sha}", + "git_tags_url": "https://api.github.com/repos/nodejs/node-auto-test/git/tags{/sha}", + "git_refs_url": "https://api.github.com/repos/nodejs/node-auto-test/git/refs{/sha}", + "trees_url": "https://api.github.com/repos/nodejs/node-auto-test/git/trees{/sha}", + "statuses_url": "https://api.github.com/repos/nodejs/node-auto-test/statuses/{sha}", + "languages_url": "https://api.github.com/repos/nodejs/node-auto-test/languages", + "stargazers_url": "https://api.github.com/repos/nodejs/node-auto-test/stargazers", + "contributors_url": "https://api.github.com/repos/nodejs/node-auto-test/contributors", + "subscribers_url": "https://api.github.com/repos/nodejs/node-auto-test/subscribers", + "subscription_url": "https://api.github.com/repos/nodejs/node-auto-test/subscription", + "commits_url": "https://api.github.com/repos/nodejs/node-auto-test/commits{/sha}", + "git_commits_url": "https://api.github.com/repos/nodejs/node-auto-test/git/commits{/sha}", + "comments_url": "https://api.github.com/repos/nodejs/node-auto-test/comments{/number}", + "issue_comment_url": "https://api.github.com/repos/nodejs/node-auto-test/issues/comments{/number}", + "contents_url": "https://api.github.com/repos/nodejs/node-auto-test/contents/{+path}", + "compare_url": "https://api.github.com/repos/nodejs/node-auto-test/compare/{base}...{head}", + "merges_url": "https://api.github.com/repos/nodejs/node-auto-test/merges", + "archive_url": "https://api.github.com/repos/nodejs/node-auto-test/{archive_format}{/ref}", + "downloads_url": "https://api.github.com/repos/nodejs/node-auto-test/downloads", + "issues_url": "https://api.github.com/repos/nodejs/node-auto-test/issues{/number}", + "pulls_url": "https://api.github.com/repos/nodejs/node-auto-test/pulls{/number}", + "milestones_url": "https://api.github.com/repos/nodejs/node-auto-test/milestones{/number}", + "notifications_url": "https://api.github.com/repos/nodejs/node-auto-test/notifications{?since,all,participating}", + "labels_url": "https://api.github.com/repos/nodejs/node-auto-test/labels{/name}", + "releases_url": "https://api.github.com/repos/nodejs/node-auto-test/releases{/id}", + "deployments_url": "https://api.github.com/repos/nodejs/node-auto-test/deployments", + "created_at": "2017-10-24T05:54:55Z", + "updated_at": "2020-07-07T23:21:44Z", + "pushed_at": "2020-07-12T14:38:35Z", + "git_url": "git://github.com/nodejs/node-auto-test.git", + "ssh_url": "git@github.com:nodejs/node-auto-test.git", + "clone_url": "https://github.com/nodejs/node-auto-test.git", + "svn_url": "https://github.com/nodejs/node-auto-test", + "homepage": "", + "size": 452674, + "stargazers_count": 7, + "watchers_count": 7, + "language": "JavaScript", + "has_issues": true, + "has_projects": true, + "has_downloads": true, + "has_wiki": true, + "has_pages": false, + "forks_count": 12, + "mirror_url": null, + "archived": false, + "disabled": false, + "open_issues_count": 4, + "license": { + "key": "other", + "name": "Other", + "spdx_id": "NOASSERTION", + "url": null, + "node_id": "MDc6TGljZW5zZTA=" + }, + "forks": 12, + "open_issues": 4, + "watchers": 7, + "default_branch": "master", + "permissions": { + "admin": true, + "push": true, + "pull": true + }, + "temp_clone_token": "", + "allow_squash_merge": true, + "allow_merge_commit": true, + "allow_rebase_merge": true, + "delete_branch_on_merge": false, + "organization": { + "login": "nodejs", + "id": 9950313, + "node_id": "MDEyOk9yZ2FuaXphdGlvbjk5NTAzMTM=", + "avatar_url": "https://avatars3.githubusercontent.com/u/9950313?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/nodejs", + "html_url": "https://github.com/nodejs", + "followers_url": "https://api.github.com/users/nodejs/followers", + "following_url": "https://api.github.com/users/nodejs/following{/other_user}", + "gists_url": "https://api.github.com/users/nodejs/gists{/gist_id}", + "starred_url": "https://api.github.com/users/nodejs/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/nodejs/subscriptions", + "organizations_url": "https://api.github.com/users/nodejs/orgs", + "repos_url": "https://api.github.com/users/nodejs/repos", + "events_url": "https://api.github.com/users/nodejs/events{/privacy}", + "received_events_url": "https://api.github.com/users/nodejs/received_events", + "type": "Organization", + "site_admin": false + }, + "network_count": 12, + "subscribers_count": 67 +} diff --git a/test/_fixtures/pull-request-create-comment.json b/test/_fixtures/pull-request-create-comment.json new file mode 100644 index 00000000..1963ad00 --- /dev/null +++ b/test/_fixtures/pull-request-create-comment.json @@ -0,0 +1,32 @@ +{ + "url": "https://api.github.com/repos/nodejs/node-auto-test/issues/comments/667621459", + "html_url": "https://github.com/nodejs/node-auto-test/pull/19#issuecomment-667621459", + "issue_url": "https://api.github.com/repos/nodejs/node-auto-test/issues/19", + "id": 667621459, + "node_id": "MDEyOklzc3VlQ29tbWVudDY2NzYyMTQ1OQ==", + "user": { + "login": "mmarchini", + "id": 4048656, + "node_id": "MDQ6VXNlcjQwNDg2NTY=", + "avatar_url": "https://avatars1.githubusercontent.com/u/4048656?u=bf848ffe5efedded28239e602bfa3cd07b8e3e05&v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/mmarchini", + "html_url": "https://github.com/mmarchini", + "followers_url": "https://api.github.com/users/mmarchini/followers", + "following_url": "https://api.github.com/users/mmarchini/following{/other_user}", + "gists_url": "https://api.github.com/users/mmarchini/gists{/gist_id}", + "starred_url": "https://api.github.com/users/mmarchini/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/mmarchini/subscriptions", + "organizations_url": "https://api.github.com/users/mmarchini/orgs", + "repos_url": "https://api.github.com/users/mmarchini/repos", + "events_url": "https://api.github.com/users/mmarchini/events{/privacy}", + "received_events_url": "https://api.github.com/users/mmarchini/received_events", + "type": "User", + "site_admin": false + }, + "created_at": "2020-08-02T03:37:18Z", + "updated_at": "2020-08-02T03:37:18Z", + "author_association": "MEMBER", + "body": "test comment", + "performed_via_github_app": null +} diff --git a/test/read-fixture.js b/test/read-fixture.js index dcf9597c..16df834a 100644 --- a/test/read-fixture.js +++ b/test/read-fixture.js @@ -5,5 +5,5 @@ const path = require('path') module.exports = function readFixture (fixtureName) { const content = fs.readFileSync(path.join(__dirname, '_fixtures', fixtureName)).toString() - return JSON.parse(content) + return fixtureName.endsWith('.json') ? JSON.parse(content) : content } diff --git a/test/unit/node-owners.test.js b/test/unit/node-owners.test.js new file mode 100644 index 00000000..3d8153a6 --- /dev/null +++ b/test/unit/node-owners.test.js @@ -0,0 +1,89 @@ +'use strict' + +const tap = require('tap') + +const { Owners } = require('../../lib/node-owners') +const readFixture = require('../read-fixture') + +const ownersFile = readFixture('CODEOWNERS') + +tap.test('single file single team match', (t) => { + const owners = Owners.fromFile(ownersFile) + t.strictDeepEqual( + owners.getOwnersForPaths([ 'file1' ]), + [ '@nodejs/test1' ] + ) + t.end() +}) + +tap.test('double file single team match', (t) => { + const owners = Owners.fromFile(ownersFile) + t.strictDeepEqual( + owners.getOwnersForPaths([ 'file1', 'file4' ]), + [ '@nodejs/test1' ] + ) + t.end() +}) + +tap.test('double file double individual team match', (t) => { + const owners = Owners.fromFile(ownersFile) + t.strictDeepEqual( + owners.getOwnersForPaths([ 'file1', 'file2' ]), + [ '@nodejs/test1', '@nodejs/test2' ] + ) + t.end() +}) + +tap.test('single file double team match', (t) => { + const owners = Owners.fromFile(ownersFile) + t.strictDeepEqual( + owners.getOwnersForPaths([ 'file3' ]), + [ '@nodejs/test1', '@nodejs/test2' ] + ) + t.end() +}) + +tap.test('double file triple team match (1 + 2)', (t) => { + const owners = Owners.fromFile(ownersFile) + t.strictDeepEqual( + owners.getOwnersForPaths([ 'file5', 'file3' ]), + [ '@nodejs/test1', '@nodejs/test2', '@nodejs/test3' ] + ) + t.end() +}) + +tap.test('folder match', (t) => { + const owners = Owners.fromFile(ownersFile) + t.strictDeepEqual( + owners.getOwnersForPaths([ 'folder1/file5' ]), + [ '@nodejs/test3' ] + ) + t.end() +}) + +tap.test('extension match', (t) => { + const owners = Owners.fromFile(ownersFile) + t.strictDeepEqual( + owners.getOwnersForPaths([ 'folder2/file1.js' ]), + [ '@nodejs/test4', '@nodejs/test5' ] + ) + t.end() +}) + +tap.test('no match', (t) => { + const owners = Owners.fromFile(ownersFile) + t.strictDeepEqual( + owners.getOwnersForPaths([ 'unknown' ]), + [ ] + ) + t.end() +}) + +tap.test('no match + single match', (t) => { + const owners = Owners.fromFile(ownersFile) + t.strictDeepEqual( + owners.getOwnersForPaths([ 'unknown', 'file1' ]), + [ '@nodejs/test1' ] + ) + t.end() +}) diff --git a/test/unit/node-repo-owners.test.js b/test/unit/node-repo-owners.test.js new file mode 100644 index 00000000..007a7edf --- /dev/null +++ b/test/unit/node-repo-owners.test.js @@ -0,0 +1,186 @@ +'use strict' + +const tap = require('tap') +const nock = require('nock') +const url = require('url') + +const { resolveOwnersThenPingPr, _testExports } = require('../../lib/node-repo') +const { + getCodeOwnersUrl, + getFiles, + getDefaultBranch, + getCodeOwnersFile, + pingOwners, + getCommentForOwners +} = _testExports +const readFixture = require('../read-fixture') + +const options = { + owner: 'nodejs', + repo: 'node-auto-test', + prId: 12345, + number: 12345, + logger: { info: () => {}, error: () => {}, debug: () => {} }, + retries: 1, + defaultBranch: 'main', + retryInterval: 10 +} + +tap.test('getCodeOwnersUrl', (t) => { + const { owner, repo, defaultBranch } = options + t.strictEqual( + getCodeOwnersUrl(owner, repo, defaultBranch), + `https://raw.githubusercontent.com/${owner}/${repo}/${defaultBranch}/.github/CODEOWNERS` + ) + t.end() +}) + +tap.test('getFiles success', async (t) => { + const fixture = readFixture('pull-request-files.json') + const scope = nock('https://api.github.com') + .filteringPath(ignoreQueryParams) + .get(`/repos/${options.owner}/${options.repo}/pulls/${options.prId}/files`) + .reply(200, fixture) + + const files = await getFiles(options) + t.strictDeepEqual(files, fixture.map(({ filename }) => filename)) + scope.done() + t.end() +}) + +tap.test('getFiles fail', async (t) => { + const scope = nock('https://api.github.com') + .filteringPath(ignoreQueryParams) + .get(`/repos/${options.owner}/${options.repo}/pulls/${options.prId}/files`) + .reply(500) + + await t.rejects(getFiles(options)) + scope.done() + t.end() +}) + +tap.test('getDefaultBranch success', async (t) => { + const fixture = readFixture('get-repository.json') + const scope = nock('https://api.github.com') + .filteringPath(ignoreQueryParams) + .get(`/repos/${options.owner}/${options.repo}`) + .reply(200, fixture) + + const defaultBranch = await getDefaultBranch(options) + t.strictDeepEqual(defaultBranch, fixture.default_branch) + scope.done() + t.end() +}) + +tap.test('getDefaultBranch empty response', async (t) => { + const scope = nock('https://api.github.com') + .filteringPath(ignoreQueryParams) + .get(`/repos/${options.owner}/${options.repo}`) + .reply(200) + + await t.rejects(getDefaultBranch(options)) + scope.done() + t.end() +}) + +tap.test('getDefaultBranch fail', async (t) => { + const scope = nock('https://api.github.com') + .filteringPath(ignoreQueryParams) + .get(`/repos/${options.owner}/${options.repo}`) + .reply(500) + + await t.rejects(getDefaultBranch(options)) + scope.done() + t.end() +}) + +tap.test('getCodeOwnersFile success', async (t) => { + const fixture = readFixture('CODEOWNERS') + const base = 'https://localhost' + const filePath = '/CODEOWNERS' + const url = `${base}${filePath}` + const scope = nock(base) + .filteringPath(ignoreQueryParams) + .get(filePath) + .reply(200, fixture) + + const file = await getCodeOwnersFile(url, options) + t.strictDeepEqual(file, fixture) + scope.done() + t.end() +}) + +tap.test('getCodeOwnersFile fail', async (t) => { + const base = 'https://localhost' + const filePath = '/CODEOWNERS' + const url = `${base}${filePath}` + const scope = nock(base) + .filteringPath(ignoreQueryParams) + .get(filePath) + .reply(500) + + await t.rejects(getCodeOwnersFile(url, options)) + scope.done() + t.end() +}) + +tap.test('pingOwners success', async (t) => { + const fixture = readFixture('pull-request-create-comment.json') + const owners = [ '@owner1', '@owner2' ] + const body = JSON.stringify({ body: getCommentForOwners(owners) }) + const scope = nock('https://api.github.com') + .filteringPath(ignoreQueryParams) + .post(`/repos/${options.owner}/${options.repo}/issues/${options.prId}/comments`, body) + .reply(201, fixture) + + await pingOwners(options, owners) + scope.done() + t.end() +}) + +tap.test('pingOwners fail', async (t) => { + const scope = nock('https://api.github.com') + .filteringPath(ignoreQueryParams) + .post(`/repos/${options.owner}/${options.repo}/issues/${options.prId}/comments`) + .reply(500) + + await t.rejects(pingOwners(options, [])) + scope.done() + t.end() +}) + +tap.test('resolveOwnersThenPingPr success', async (t) => { + const owners = [ '@nodejs/team', '@nodejs/team2' ] + const scopes = [ + nock('https://api.github.com') + .filteringPath(ignoreQueryParams) + .get(`/repos/${options.owner}/${options.repo}/pulls/${options.prId}/files`) + .reply(200, readFixture('pull-request-files.json')), + nock('https://api.github.com') + .filteringPath(ignoreQueryParams) + .get(`/repos/${options.owner}/${options.repo}`) + .reply(200, readFixture('get-repository.json')), + + nock('https://raw.githubusercontent.com') + .filteringPath(ignoreQueryParams) + .get(`/${options.owner}/${options.repo}/master/.github/CODEOWNERS`) + .reply(200, readFixture('CODEOWNERS')), + nock('https://api.github.com') + .filteringPath(ignoreQueryParams) + .post(`/repos/${options.owner}/${options.repo}/issues/${options.prId}/comments`, JSON.stringify({ body: getCommentForOwners(owners) })) + .reply(201, readFixture('pull-request-create-comment.json')) + ] + + // If promise doesn't reject we succeeded. The last post + // is tested by nock + console.log(await resolveOwnersThenPingPr(options, owners)) + + for (const scope of scopes) { + scope.done() + } + t.end() +}) + +function ignoreQueryParams (pathAndQuery) { + return url.parse(pathAndQuery, true).pathname +}