Skip to content

Commit 1d60392

Browse files
committed
v2.13.0: Added exclude option to filter reviewers
1 parent 1460cd0 commit 1d60392

21 files changed

Lines changed: 267 additions & 72 deletions

File tree

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
# Changelog
22
All notable changes to this project will be documented in this file.
33

4+
## [2.13.0] - 2024-02-10
5+
### Added
6+
- `exclude` option to exclude specific users from the stats.
7+
8+
### Fixed
9+
- Reduces the block size in the Slack messages to prevent hitting the characters limit.
10+
411
## [2.12.0] - 2024-02-06
512
### Changed
613
- [#85](https://github.com/flowwer-dev/pull-request-stats/pull/85) Use Node v20 (by [antonindrawan](https://github.com/antonindrawan))

README.md

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ Just add this action to one of your [workflow files](https://docs.github.com/en/
3939
uses: flowwer-dev/pull-request-stats@master
4040
```
4141
42+
If you are getting an empty table or an error, check the [troubleshooting section](#troubleshooting).
43+
4244
### Action inputs
4345
4446
The possible inputs for this action are:
@@ -47,18 +49,19 @@ The possible inputs for this action are:
4749
| --------- | ----------- | ------- |
4850
| `token` | A [Personal Access Token](https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token) with `repo` permissions. Required to calculate stats for an organization or multiple repos. | `GITHUB_TOKEN` |
4951
| `repositories` | A comma-separated list of GitHub repositories to calculate the stats, e.g. `username/repo1,username/repo2`. When specifying other repo(s), **it is mandatory to pass a Personal Access Token** in the `token` parameter.| Current repository |
50-
| `organization` | If you prefer, you may specify your organization's name to calculate the stats across all of its repos. When specifying an organization, **it is mandatory to pass a Personal Access Token** in the `token` parameter. | `null`|
52+
| `organization` | If you prefer, you may specify your organization's name to calculate the stats across all of its repos. When specifying an organization, **it is mandatory to pass a Personal Access Token** in the `token` parameter. | `null` |
5153
| `period` | The period used to calculate the stats, expressed in days. | `30` |
52-
| `limit` | The maximum number of rows to display in the table. A value of `0` means unlimited. |`0`|
54+
| `limit` | The maximum number of rows to display in the table. A value of `0` means unlimited. | `0` |
5355
| `charts` | Whether to add a chart to the start. Possible values: `true` or `false`. | `false` |
5456
| `disableLinks` | If `true`, removes the links to the detailed charts. Possible values: `true` or `false`. | `false` |
5557
| `sortBy` | The column used to sort the data. Possible values: `REVIEWS`, `TIME`, `COMMENTS`. | `REVIEWS` |
5658
| `publishAs` | Where to publish the results. Possible values: as a `COMMENT`, on the pull request `DESCRIPTION`, or publish `NONE`. | `COMMENT` |
59+
| `exclude` | A comma-separated list of usernames (case-insensitive) to be excluded from the results (e.g. `username1,username2`), or a regular expression enclosed between slashes (eg. `/^bot/i` will exclude all usernames that begin with "bot"). | `null` |
5760
| `telemetry` | Indicates if the action is allowed to send monitoring data to the developer. This data is [minimal](/src/services/telemetry/sendStart.js) and helps me improve this action. **This option is a premium feature reserved for [sponsors](#premium-features-).** |`true`|
58-
| `slackWebhook` | **🔥 New.** A Slack webhook URL to post resulting stats. **This option is a premium feature reserved for [sponsors](#premium-features-).** See [full documentation here](/docs/slack.md). |`null`|
59-
| `slackChannel` | The Slack channel where stats will be posted. Include the `#` character (eg. `#mychannel`). Required when a `slackWebhook` is configured. |`null`|
60-
| `teamsWebhook` | **🔥 New.** A Microsoft Teams webhook URL to post resulting stats. **This option is a premium feature reserved for [sponsors](#premium-features-).** See [full documentation here](/docs/teams.md). |`null`|
61-
| `webhook` | **🔥 New.** A webhook URL to send the resulting stats as JSON (integrate with Zapier, IFTTT...). See [full documentation here](/docs/webhook.md). |`null`|
61+
| `slackWebhook` | **🔥 New.** A Slack webhook URL to post resulting stats. **This option is a premium feature reserved for [sponsors](#premium-features-).** See [full documentation here](/docs/slack.md). | `null` |
62+
| `slackChannel` | The Slack channel where stats will be posted. Include the `#` character (eg. `#mychannel`). Required when a `slackWebhook` is configured. | `null` |
63+
| `teamsWebhook` | **🔥 New.** A Microsoft Teams webhook URL to post resulting stats. **This option is a premium feature reserved for [sponsors](#premium-features-).** See [full documentation here](/docs/teams.md). | `null` |
64+
| `webhook` | **🔥 New.** A webhook URL to send the resulting stats as JSON (integrate with Zapier, IFTTT...). See [full documentation here](/docs/webhook.md). | `null` |
6265

6366

6467
### Action outputs
@@ -179,6 +182,19 @@ Check the guide for the tool you want to integrate:
179182
1. Make sure the repositories have pull request reviews during the configured `period`.
180183
2. When specifying `repositories` or `organization` parameters, a [Personal Access Token](https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token) is required in the `token` parameter.
181184
3. If providing a Personal Access Token, ensure it has the `repo` permission for the projects you want.
185+
4. If you are not providing a Personal Access Token (thus, the action is using the default `GITHUB_TOKEN`), make sure the job has the `contents: read` and `pull-requests: write` [permissions](https://docs.github.com/en/actions/using-jobs/assigning-permissions-to-jobs#defining-access-for-the-github_token-scopes) While these permissions are typically provided by default, certain organizations may customize or overwrite them.
186+
187+
```yml
188+
jobs:
189+
stats:
190+
runs-on: ubuntu-latest
191+
permissions:
192+
contents: read
193+
pull-requests: write
194+
steps:
195+
- name: Run pull request stats
196+
uses: flowwer-dev/pull-request-stats@master
197+
```
182198
</details>
183199

184200
<details>

action.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ inputs:
3535
description: 'Where to publish the results. Possible values: "COMMENT", "DESCRIPTION" or "NONE"'
3636
required: false
3737
default: 'COMMENT'
38+
exclude:
39+
description: 'A list or regular expression to exclude users from the stats'
40+
required: false
3841
disableLinks:
3942
description: 'Prevents from adding any external links in the stats'
4043
required: false

dist/index.js

Lines changed: 77 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -41192,11 +41192,14 @@ module.exports = parseParams
4119241192
/***/ 1855:
4119341193
/***/ ((module) => {
4119441194

41195-
const getSlackCharsLimit = () => 39000;
41196-
const getTeamsBytesLimit = () => 27000;
41195+
const getSlackLimits = () => ({
41196+
chars: 30_000,
41197+
blocks: 50,
41198+
});
41199+
const getTeamsBytesLimit = () => 27_000;
4119741200

4119841201
module.exports = {
41199-
getSlackCharsLimit,
41202+
getSlackLimits,
4120041203
getTeamsBytesLimit,
4120141204
};
4120241205

@@ -41288,7 +41291,7 @@ const run = async (params) => {
4128841291
});
4128941292
core.info(`Found ${pulls.length} pull requests to analyze`);
4129041293

41291-
const reviewersRaw = getReviewers(pulls);
41294+
const reviewersRaw = getReviewers(pulls, { excludeStr: params.excludeStr });
4129241295
core.info(`Analyzed stats for ${reviewersRaw.length} pull request reviewers`);
4129341296

4129441297
const reviewers = setUpReviewers({
@@ -42062,6 +42065,18 @@ module.exports = (reviews) => {
4206242065
};
4206342066

4206442067

42068+
/***/ }),
42069+
42070+
/***/ 3966:
42071+
/***/ ((module) => {
42072+
42073+
module.exports = (exclude, username) => {
42074+
if (exclude.test) return !exclude.test(username);
42075+
if (exclude.includes) return !exclude.includes(username);
42076+
return true;
42077+
};
42078+
42079+
4206542080
/***/ }),
4206642081

4206742082
/***/ 9633:
@@ -42100,12 +42115,43 @@ module.exports = (pulls) => {
4210042115
/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => {
4210142116

4210242117
const calculateReviewsStats = __nccwpck_require__(3753);
42118+
const filterReviewer = __nccwpck_require__(3966);
42119+
const parseExclude = __nccwpck_require__(7960);
4210342120
const groupReviews = __nccwpck_require__(9633);
4210442121

42105-
module.exports = (pulls) => groupReviews(pulls).map(({ author, reviews }) => {
42106-
const stats = calculateReviewsStats(reviews);
42107-
return { author, reviews, stats };
42108-
});
42122+
module.exports = (pulls, { excludeStr } = {}) => {
42123+
const exclude = parseExclude(excludeStr);
42124+
return groupReviews(pulls)
42125+
.filter(({ author }) => filterReviewer(exclude, author.login))
42126+
.map(({ author, reviews }) => {
42127+
const stats = calculateReviewsStats(reviews);
42128+
return { author, reviews, stats };
42129+
});
42130+
};
42131+
42132+
42133+
/***/ }),
42134+
42135+
/***/ 7960:
42136+
/***/ ((module) => {
42137+
42138+
const REGEXP_PATTERN = /^\/.+\/[a-z]*$/;
42139+
42140+
// Github usernames can only contain alphanumeric characters and dashes (-)
42141+
const sanitize = (str = '') => (str || '').replace(/[^-a-zA-Z0-9]/g, '').toLowerCase();
42142+
42143+
const isRegExp = (str) => REGEXP_PATTERN.test(str);
42144+
42145+
const parseRegExp = (str) => {
42146+
const [pattern, flags] = str.split('/').slice(1);
42147+
return new RegExp(pattern, flags);
42148+
};
42149+
42150+
module.exports = (excludeStr) => {
42151+
if (!sanitize(excludeStr)) return [];
42152+
if (isRegExp(excludeStr)) return parseRegExp(excludeStr);
42153+
return excludeStr.split(',').map(sanitize);
42154+
};
4210942155

4211042156

4211142157
/***/ }),
@@ -43086,13 +43132,10 @@ module.exports = {
4308643132
/***/ ((module) => {
4308743133

4308843134
class BaseSplitter {
43089-
constructor({ message, limit = null }) {
43090-
this.limit = limit || this.constructor.defaultLimit();
43135+
constructor({ message, limit = null, maxBlocksLength = null }) {
4309143136
this.message = message;
43092-
}
43093-
43094-
static defaultLimit() {
43095-
return Infinity;
43137+
this.limit = limit || Infinity;
43138+
this.maxBlocksLength = maxBlocksLength || Infinity;
4309643139
}
4309743140

4309843141
get blockSize() {
@@ -43118,10 +43161,13 @@ class BaseSplitter {
4311843161
const blocksCount = this.constructor.getBlocksCount(message);
4311943162
const currentSize = this.constructor.calculateSize(message);
4312043163
const diff = currentSize - this.limit;
43121-
if (diff < 0 || blocksCount === 1) return 0;
43164+
const onLimit = diff <= 0 && blocksCount <= this.maxBlocksLength;
43165+
if (onLimit || blocksCount === 1) return 0;
4312243166

4312343167
const blocksSpace = Math.ceil(diff / this.blockSize);
43124-
const blocksToSplit = Math.max(1, Math.min(blocksCount - 1, blocksSpace));
43168+
const upperBound = Math.min(blocksCount - 1, blocksSpace);
43169+
const exceedingBlocks = Math.max(0, blocksCount - this.maxBlocksLength);
43170+
const blocksToSplit = Math.max(1, upperBound, exceedingBlocks);
4312543171
const [firsts] = this.constructor.splitBlocks(message, blocksToSplit);
4312643172
return this.calculateBlocksToSplit(firsts) || blocksToSplit;
4312743173
}
@@ -43165,13 +43211,18 @@ module.exports = {
4316543211
/***/ 2843:
4316643212
/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => {
4316743213

43168-
const { getSlackCharsLimit } = __nccwpck_require__(1855);
43214+
const { getSlackLimits } = __nccwpck_require__(1855);
4316943215
const { median } = __nccwpck_require__(9988);
4317043216
const BaseSplitter = __nccwpck_require__(7027);
4317143217

4317243218
class SlackSplitter extends BaseSplitter {
43173-
static defaultLimit() {
43174-
return getSlackCharsLimit();
43219+
constructor(args = {}) {
43220+
const limits = getSlackLimits();
43221+
super({
43222+
...args,
43223+
limit: limits.chars,
43224+
maxBlocksLength: limits.blocks,
43225+
});
4317543226
}
4317643227

4317743228
static splitBlocks(message, count) {
@@ -43212,8 +43263,11 @@ const { median } = __nccwpck_require__(9988);
4321243263
const BaseSplitter = __nccwpck_require__(7027);
4321343264

4321443265
class TeamsSplitter extends BaseSplitter {
43215-
static defaultLimit() {
43216-
return getTeamsBytesLimit();
43266+
constructor(args = {}) {
43267+
super({
43268+
...args,
43269+
limit: getTeamsBytesLimit(),
43270+
});
4321743271
}
4321843272

4321943273
static splitBlocks(body, count) {
@@ -47978,7 +48032,7 @@ module.exports = JSON.parse('{"name":"mixpanel","description":"A simple server-s
4797848032
/***/ ((module) => {
4797948033

4798048034
"use strict";
47981-
module.exports = JSON.parse('{"name":"pull-request-stats","version":"2.12.0","description":"Github action to print relevant stats about Pull Request reviewers","main":"dist/index.js","type":"commonjs","scripts":{"build":"eslint src && ncc build src/index.js -o dist -a","test":"jest","lint":"eslint ./"},"keywords":[],"author":"Manuel de la Torre","license":"MIT","jest":{"testEnvironment":"node","testMatch":["**/?(*.)+(spec|test).[jt]s?(x)"]},"dependencies":{"@actions/core":"^1.10.1","@actions/github":"^6.0.0","axios":"^1.6.7","humanize-duration":"^3.31.0","i18n-js":"^3.9.2","jsurl":"^0.1.5","lodash.get":"^4.4.2","markdown-table":"^2.0.0","mixpanel":"^0.18.0"},"devDependencies":{"@vercel/ncc":"^0.38.1","eslint":"^8.56.0","eslint-config-airbnb-base":"^15.0.0","eslint-plugin-import":"^2.29.1","eslint-plugin-jest":"^27.6.3","jest":"^29.7.0"},"funding":"https://github.com/sponsors/manuelmhtr","packageManager":"yarn@4.1.0"}');
48035+
module.exports = JSON.parse('{"name":"pull-request-stats","version":"2.13.0","description":"Github action to print relevant stats about Pull Request reviewers","main":"dist/index.js","type":"commonjs","scripts":{"build":"eslint src && ncc build src/index.js -o dist -a","test":"jest","lint":"eslint ./"},"keywords":[],"author":"Manuel de la Torre","license":"MIT","jest":{"testEnvironment":"node","testMatch":["**/?(*.)+(spec|test).[jt]s?(x)"]},"dependencies":{"@actions/core":"^1.10.1","@actions/github":"^6.0.0","axios":"^1.6.7","humanize-duration":"^3.31.0","i18n-js":"^3.9.2","jsurl":"^0.1.5","lodash.get":"^4.4.2","markdown-table":"^2.0.0","mixpanel":"^0.18.0"},"devDependencies":{"@vercel/ncc":"^0.38.1","eslint":"^8.56.0","eslint-config-airbnb-base":"^15.0.0","eslint-plugin-import":"^2.29.1","eslint-plugin-jest":"^27.6.3","jest":"^29.7.0"},"funding":"https://github.com/sponsors/manuelmhtr","packageManager":"yarn@4.1.0"}');
4798248036

4798348037
/***/ }),
4798448038

@@ -48086,6 +48140,7 @@ const getParams = () => {
4808648140
disableLinks: core.getBooleanInput('disableLinks') || core.getBooleanInput('disable-links'),
4808748141
pullRequestId: getPrId(),
4808848142
limit: parseInt(core.getInput('limit'), 10),
48143+
excludeStr: core.getInput('exclude'),
4808948144
telemetry: core.getBooleanInput('telemetry'),
4809048145
webhook: core.getInput('webhook'),
4809148146
slack: {

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "pull-request-stats",
3-
"version": "2.12.0",
3+
"version": "2.13.0",
44
"description": "Github action to print relevant stats about Pull Request reviewers",
55
"main": "dist/index.js",
66
"type": "commonjs",

src/config/index.js

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
1-
const getSlackCharsLimit = () => 39000;
2-
const getTeamsBytesLimit = () => 27000;
1+
const getSlackLimits = () => ({
2+
chars: 30_000,
3+
blocks: 50,
4+
});
5+
const getTeamsBytesLimit = () => 27_000;
36

47
module.exports = {
5-
getSlackCharsLimit,
8+
getSlackLimits,
69
getTeamsBytesLimit,
710
};

src/execute.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ const run = async (params) => {
5050
});
5151
core.info(`Found ${pulls.length} pull requests to analyze`);
5252

53-
const reviewersRaw = getReviewers(pulls);
53+
const reviewersRaw = getReviewers(pulls, { excludeStr: params.excludeStr });
5454
core.info(`Analyzed stats for ${reviewersRaw.length} pull request reviewers`);
5555

5656
const reviewers = setUpReviewers({

src/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ const getParams = () => {
3737
disableLinks: core.getBooleanInput('disableLinks') || core.getBooleanInput('disable-links'),
3838
pullRequestId: getPrId(),
3939
limit: parseInt(core.getInput('limit'), 10),
40+
excludeStr: core.getInput('exclude'),
4041
telemetry: core.getBooleanInput('telemetry'),
4142
webhook: core.getInput('webhook'),
4243
slack: {
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
const filterReviewer = require('../filterReviewer');
2+
3+
describe('Interactors | getReviewers | .filterReviewer', () => {
4+
const reviewers = [
5+
'manuelmhtr',
6+
'jartmez',
7+
'bot1',
8+
'bot2',
9+
];
10+
11+
it('filters out reviewers by a list of usernames', () => {
12+
const exclude = ['manuelmhtr', 'jartmez'];
13+
const results = reviewers.filter((reviewer) => filterReviewer(exclude, reviewer));
14+
expect(results.length).toEqual(2);
15+
expect(results).toEqual([
16+
'bot1',
17+
'bot2',
18+
]);
19+
});
20+
21+
it('filters out reviewers by a regular expression', () => {
22+
const exclude = /bot/;
23+
const results = reviewers.filter((reviewer) => filterReviewer(exclude, reviewer));
24+
expect(results.length).toEqual(2);
25+
expect(results).toEqual([
26+
'manuelmhtr',
27+
'jartmez',
28+
]);
29+
});
30+
});

src/interactors/getReviewers/__tests__/index.test.js

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
const input = require('./mocks/pullRequests');
22
const getReviewers = require('../index');
33

4+
const getAuthors = (reviewers) => reviewers.map((r) => r.author.login);
5+
46
describe('Interactors | getReviewers', () => {
57
it('groups reviews by author and calculate its stats', () => {
68
const result = getReviewers(input);
79
expect(result.length).toEqual(2);
8-
9-
const authors = result.map((r) => r.author.login);
10-
expect(authors).toContain('manuelmhtr', 'jartmez');
10+
expect(getAuthors(result)).toContain('manuelmhtr', 'jartmez');
1111

1212
result.forEach((reviewer) => {
1313
expect(reviewer).toHaveProperty('author');
@@ -21,4 +21,10 @@ describe('Interactors | getReviewers', () => {
2121
expect(reviewer.stats).toHaveProperty('timeToReview');
2222
});
2323
});
24+
25+
it('excludes reviewers when the option is passed', () => {
26+
const result = getReviewers(input, { excludeStr: 'manuelmhtr' });
27+
expect(result.length).toEqual(1);
28+
expect(getAuthors(result)).not.toContain('manuelmhtr');
29+
});
2430
});

0 commit comments

Comments
 (0)