diff --git a/README.md b/README.md index d0fb81e..d7dc03d 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ Another hacky way (no code) to find the MS Teams UPN is the following: open MS T ### ignore-label -Ignore Pull Requests with that label, eg: `no-reminder` (optional). +Ignore Pull Requests with that label(s), eg: `no-reminder` or `no-reminder,ignore me` (optional). ## Example usage @@ -46,7 +46,7 @@ jobs: pr-reviews-reminder: runs-on: ubuntu-latest steps: - - uses: davideviolante/pr-reviews-reminder-action@v2.3.2 + - uses: davideviolante/pr-reviews-reminder-action@v2.4.0 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: @@ -54,7 +54,7 @@ jobs: provider: '' # Required (slack or msteams) channel: '' # Optional, eg: #general github-provider-map: '' # Optional, eg: DavideViolante:UEABCDEFG,foobar:UAABCDEFG - ignore-label: '' # Optional, eg: no-reminder + ignore-label: '' # Optional, eg: no-reminder,ignore me ``` ## Bug or feedback? diff --git a/dist/index.js b/dist/index.js index 39a4575..6868797 100644 --- a/dist/index.js +++ b/dist/index.js @@ -16,13 +16,13 @@ function getPullRequestsToReview(pullRequests) { /** * Filter Pull Requests without a specific label * @param {Array} pullRequests Pull Requests to filter - * @param {String} ignoreLabel Pull Request label to ignore + * @param {String} ignoreLabels Pull Request label(s) to ignore * @return {Array} Pull Requests without a specific label */ -function getPullRequestsWithoutLabel(pullRequests, ignoreLabel) { - return pullRequests.filter((pr) => - !((pr.labels || []).some((label) => label.name === ignoreLabel)), - ); +function getPullRequestsWithoutLabel(pullRequests, ignoreLabels) { + const ignoreLabelsArray = ignoreLabels.replace(/\s+/g, '').split(','); // ['ignore1', 'ignore2', ...] + const ignoreLabelsSet = new Set(ignoreLabelsArray); + return pullRequests.filter((pr) => !((pr.labels || []).some((label) => ignoreLabelsSet.has(label.name)))); } /** @@ -6464,7 +6464,7 @@ module.exports = require("zlib"); /***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { "use strict"; -// Axios v1.2.3 Copyright (c) 2023 Matt Zabriskie and contributors +// Axios v1.3.0 Copyright (c) 2023 Matt Zabriskie and contributors const FormData$1 = __nccwpck_require__(4334); @@ -7004,7 +7004,7 @@ const matchAll = (regExp, str) => { const isHTMLForm = kindOfTest('HTMLFormElement'); const toCamelCase = str => { - return str.toLowerCase().replace(/[_-\s]([a-z\d])(\w*)/g, + return str.toLowerCase().replace(/[-_\s]([a-z\d])(\w*)/g, function replacer(m, p1, p2) { return p1.toUpperCase() + p2; } @@ -7088,6 +7088,37 @@ const toFiniteNumber = (value, defaultValue) => { return Number.isFinite(value) ? value : defaultValue; }; +const ALPHA = 'abcdefghijklmnopqrstuvwxyz'; + +const DIGIT = '0123456789'; + +const ALPHABET = { + DIGIT, + ALPHA, + ALPHA_DIGIT: ALPHA + ALPHA.toUpperCase() + DIGIT +}; + +const generateString = (size = 16, alphabet = ALPHABET.ALPHA_DIGIT) => { + let str = ''; + const {length} = alphabet; + while (size--) { + str += alphabet[Math.random() * length|0]; + } + + return str; +}; + +/** + * If the thing is a FormData object, return true, otherwise return false. + * + * @param {unknown} thing - The thing to check. + * + * @returns {boolean} + */ +function isSpecCompliantForm(thing) { + return !!(thing && isFunction(thing.append) && thing[Symbol.toStringTag] === 'FormData' && thing[Symbol.iterator]); +} + const toJSONObject = (obj) => { const stack = new Array(10); @@ -7165,6 +7196,9 @@ const utils = { findKey, global: _global, isContextDefined, + ALPHABET, + generateString, + isSpecCompliantForm, toJSONObject }; @@ -7318,17 +7352,6 @@ const predicates = utils.toFlatObject(utils, {}, null, function filter(prop) { return /^is[A-Z]/.test(prop); }); -/** - * If the thing is a FormData object, return true, otherwise return false. - * - * @param {unknown} thing - The thing to check. - * - * @returns {boolean} - */ -function isSpecCompliant(thing) { - return thing && utils.isFunction(thing.append) && thing[Symbol.toStringTag] === 'FormData' && thing[Symbol.iterator]; -} - /** * Convert a data object to FormData * @@ -7376,7 +7399,7 @@ function toFormData(obj, formData, options) { const dots = options.dots; const indexes = options.indexes; const _Blob = options.Blob || typeof Blob !== 'undefined' && Blob; - const useBlob = _Blob && isSpecCompliant(formData); + const useBlob = _Blob && utils.isSpecCompliantForm(formData); if (!utils.isFunction(visitor)) { throw new TypeError('visitor must be a function'); @@ -8163,8 +8186,20 @@ class AxiosHeaders { return deleted; } - clear() { - return Object.keys(this).forEach(this.delete.bind(this)); + clear(matcher) { + const keys = Object.keys(this); + let i = keys.length; + let deleted = false; + + while (i--) { + const key = keys[i]; + if(!matcher || matchHeaderValue(this, this[key], key, matcher)) { + delete this[key]; + deleted = true; + } + } + + return deleted; } normalize(format) { @@ -8255,7 +8290,7 @@ class AxiosHeaders { } } -AxiosHeaders.accessor(['Content-Type', 'Content-Length', 'Accept', 'Accept-Encoding', 'User-Agent']); +AxiosHeaders.accessor(['Content-Type', 'Content-Length', 'Accept', 'Accept-Encoding', 'User-Agent', 'Authorization']); utils.freezeMethods(AxiosHeaders.prototype); utils.freezeMethods(AxiosHeaders); @@ -8377,7 +8412,7 @@ function buildFullPath(baseURL, requestedURL) { return requestedURL; } -const VERSION = "1.2.3"; +const VERSION = "1.3.0"; function parseProtocol(url) { const match = /^([-+\w]{1,25})(:?\/\/|:)/.exec(url); @@ -8699,6 +8734,154 @@ class AxiosTransformStream extends stream__default["default"].Transform{ const AxiosTransformStream$1 = AxiosTransformStream; +const {asyncIterator} = Symbol; + +const readBlob = async function* (blob) { + if (blob.stream) { + yield* blob.stream(); + } else if (blob.arrayBuffer) { + yield await blob.arrayBuffer(); + } else if (blob[asyncIterator]) { + yield* blob[asyncIterator](); + } else { + yield blob; + } +}; + +const readBlob$1 = readBlob; + +const BOUNDARY_ALPHABET = utils.ALPHABET.ALPHA_DIGIT + '-_'; + +const textEncoder = new TextEncoder(); + +const CRLF = '\r\n'; +const CRLF_BYTES = textEncoder.encode(CRLF); +const CRLF_BYTES_COUNT = 2; + +class FormDataPart { + constructor(name, value) { + const {escapeName} = this.constructor; + const isStringValue = utils.isString(value); + + let headers = `Content-Disposition: form-data; name="${escapeName(name)}"${ + !isStringValue && value.name ? `; filename="${escapeName(value.name)}"` : '' + }${CRLF}`; + + if (isStringValue) { + value = textEncoder.encode(String(value).replace(/\r?\n|\r\n?/g, CRLF)); + } else { + headers += `Content-Type: ${value.type || "application/octet-stream"}${CRLF}`; + } + + this.headers = textEncoder.encode(headers + CRLF); + + this.contentLength = isStringValue ? value.byteLength : value.size; + + this.size = this.headers.byteLength + this.contentLength + CRLF_BYTES_COUNT; + + this.name = name; + this.value = value; + } + + async *encode(){ + yield this.headers; + + const {value} = this; + + if(utils.isTypedArray(value)) { + yield value; + } else { + yield* readBlob$1(value); + } + + yield CRLF_BYTES; + } + + static escapeName(name) { + return String(name).replace(/[\r\n"]/g, (match) => ({ + '\r' : '%0D', + '\n' : '%0A', + '"' : '%22', + }[match])); + } +} + +const formDataToStream = (form, headersHandler, options) => { + const { + tag = 'form-data-boundary', + size = 25, + boundary = tag + '-' + utils.generateString(size, BOUNDARY_ALPHABET) + } = options || {}; + + if(!utils.isFormData(form)) { + throw TypeError('FormData instance required'); + } + + if (boundary.length < 1 || boundary.length > 70) { + throw Error('boundary must be 10-70 characters long') + } + + const boundaryBytes = textEncoder.encode('--' + boundary + CRLF); + const footerBytes = textEncoder.encode('--' + boundary + '--' + CRLF + CRLF); + let contentLength = footerBytes.byteLength; + + const parts = Array.from(form.entries()).map(([name, value]) => { + const part = new FormDataPart(name, value); + contentLength += part.size; + return part; + }); + + contentLength += boundaryBytes.byteLength * parts.length; + + contentLength = utils.toFiniteNumber(contentLength); + + const computedHeaders = { + 'Content-Type': `multipart/form-data; boundary=${boundary}` + }; + + if (Number.isFinite(contentLength)) { + computedHeaders['Content-Length'] = contentLength; + } + + headersHandler && headersHandler(computedHeaders); + + return stream.Readable.from((async function *() { + for(const part of parts) { + yield boundaryBytes; + yield* part.encode(); + } + + yield footerBytes; + })()); +}; + +const formDataToStream$1 = formDataToStream; + +class ZlibHeaderTransformStream extends stream__default["default"].Transform { + __transform(chunk, encoding, callback) { + this.push(chunk); + callback(); + } + + _transform(chunk, encoding, callback) { + if (chunk.length !== 0) { + this._transform = this.__transform; + + // Add Default Compression headers if no zlib headers are present + if (chunk[0] !== 120) { // Hex: 78 + const header = Buffer.alloc(2); + header[0] = 120; // Hex: 78 + header[1] = 156; // Hex: 9C + this.push(header, encoding); + } + } + + this.__transform(chunk, encoding, callback); + } +} + +const ZlibHeaderTransformStream$1 = ZlibHeaderTransformStream; + const zlibOptions = { flush: zlib__default["default"].constants.Z_SYNC_FLUSH, finishFlush: zlib__default["default"].constants.Z_SYNC_FLUSH @@ -8921,9 +9104,27 @@ const httpAdapter = isHttpAdapterSupported && function httpAdapter(config) { let maxUploadRate = undefined; let maxDownloadRate = undefined; - // support for https://www.npmjs.com/package/form-data api - if (utils.isFormData(data) && utils.isFunction(data.getHeaders)) { + // support for spec compliant FormData objects + if (utils.isSpecCompliantForm(data)) { + const userBoundary = headers.getContentType(/boundary=([-_\w\d]{10,70})/i); + + data = formDataToStream$1(data, (formHeaders) => { + headers.set(formHeaders); + }, { + tag: `axios-${VERSION}-boundary`, + boundary: userBoundary && userBoundary[1] || undefined + }); + // support for https://www.npmjs.com/package/form-data api + } else if (utils.isFormData(data) && utils.isFunction(data.getHeaders)) { headers.set(data.getHeaders()); + if (utils.isFunction(data.getLengthSync)) { // check if the undocumented API exists + const knownLength = data.getLengthSync(); + !utils.isUndefined(knownLength) && headers.setContentLength(knownLength, false); + } + } else if (utils.isBlob(data)) { + data.size && headers.setContentType(data.type || 'application/octet-stream'); + headers.setContentLength(data.size || 0); + data = stream__default["default"].Readable.from(readBlob$1(data)); } else if (data && !utils.isStream(data)) { if (Buffer.isBuffer(data)) ; else if (utils.isArrayBuffer(data)) { data = Buffer.from(new Uint8Array(data)); @@ -8938,7 +9139,7 @@ const httpAdapter = isHttpAdapterSupported && function httpAdapter(config) { } // Add Content-Length header if data exists - headers.set('Content-Length', data.length, false); + headers.setContentLength(data.length, false); if (config.maxBodyLength > -1 && data.length > config.maxBodyLength) { return reject(new AxiosError( @@ -9102,7 +9303,15 @@ const httpAdapter = isHttpAdapterSupported && function httpAdapter(config) { case 'x-gzip': case 'compress': case 'x-compress': + // add the unzipper to the body stream processing pipeline + streams.push(zlib__default["default"].createUnzip(zlibOptions)); + + // remove the content-encoding in order to not confuse downstream operations + delete res.headers['content-encoding']; + break; case 'deflate': + streams.push(new ZlibHeaderTransformStream$1()); + // add the unzipper to the body stream processing pipeline streams.push(zlib__default["default"].createUnzip(zlibOptions)); diff --git a/functions.js b/functions.js index 47472d8..53cbc6f 100644 --- a/functions.js +++ b/functions.js @@ -10,13 +10,13 @@ function getPullRequestsToReview(pullRequests) { /** * Filter Pull Requests without a specific label * @param {Array} pullRequests Pull Requests to filter - * @param {String} ignoreLabel Pull Request label to ignore + * @param {String} ignoreLabels Pull Request label(s) to ignore * @return {Array} Pull Requests without a specific label */ -function getPullRequestsWithoutLabel(pullRequests, ignoreLabel) { - return pullRequests.filter((pr) => - !((pr.labels || []).some((label) => label.name === ignoreLabel)), - ); +function getPullRequestsWithoutLabel(pullRequests, ignoreLabels) { + const ignoreLabelsArray = ignoreLabels.replace(/\s*,\s*/g, ',').split(','); // ['ignore1', 'ignore2', ...] + const ignoreLabelsSet = new Set(ignoreLabelsArray); + return pullRequests.filter((pr) => !((pr.labels || []).some((label) => ignoreLabelsSet.has(label.name)))); } /** diff --git a/package-lock.json b/package-lock.json index 7817c51..7717d6f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "pr-reviews-reminder-action", - "version": "2.3.2", + "version": "2.4.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "pr-reviews-reminder-action", - "version": "2.3.2", + "version": "2.4.0", "license": "MIT", "dependencies": { "@actions/core": "^1.10.0", diff --git a/package.json b/package.json index b125ea3..5d53f73 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pr-reviews-reminder-action", - "version": "2.3.2", + "version": "2.4.0", "description": "Reminder for Pull Request pending reviews", "scripts": { "build": "ncc build index.js", diff --git a/test/test.js b/test/test.js index b4b4459..4438b58 100644 --- a/test/test.js +++ b/test/test.js @@ -216,6 +216,19 @@ describe('Pull Request Reviews Reminder Action tests', () => { delete mockPullRequests[3].labels; }); + it('Should get pull requests with requested reviewers and skip those with ignore label (array)', () => { + mockPullRequests[1].labels = [{ name: 'ignore me' }, { name: 'test' }]; + mockPullRequests[2].labels = [{ name: 'ignore2' }]; + mockPullRequests[3].labels = [{ name: 'test' }, { name: 'ignore3' }]; + const pullRequests = getPullRequestsToReview(mockPullRequests); + assert.strictEqual(pullRequests.length, 4); + const pullRequestsWithoutLabel = getPullRequestsWithoutLabel(pullRequests, 'ignore1, ignore me ,ignore2 , ignore3,ignore'); + assert.strictEqual(pullRequestsWithoutLabel.length, 2); + delete mockPullRequests[1].labels; + delete mockPullRequests[2].labels; + delete mockPullRequests[3].labels; + }); + it('Should count the total number of reviewers in the pull requests', () => { const total = getPullRequestsReviewersCount(mockPullRequests); assert.strictEqual(total, 4);