Repository: release-drafter/release-drafter
Pull request #898 (2021-06-19 18:20:25)

Added exclude-contributors and no-contributors-template configuration options
As I mentioned in #569, I think it would be nice if we, as repository owners, would be able to remove ourselves from the list of contributors, so that we can more easily highlight 3rd party contributions.

I'm no JS developer myself and know precious little about Node, but I've made a best effort at adding a configuration option named `exclude-contributors` which removes specified usernames from the `$CONTRIBUTORS` variable.

I decided to not exclude the names that come from `commit.author.name` because if I understand the API correctly, they are not necessarily unique.

PS: I'm not sure if I committed all the necessary files, so that's something that the reviewer should double-check. I'm also not sure if I set up my development environment properly; there was one test suite that failed, even before I started making my changes:
```
 test/config.test.js
  ● Test suite failed to run

    Call retries were exceeded

      at ChildProcessWorker.initialize (node_modules/jest-worker/build/workers/ChildProcessWorker.js:193:21)
```

ThomasKasene: Actually, I just realized that since I'm filtering values away, there's a chance the `else`-block here will end in an `ArrayIndexOutOfBoundsException` or something similar:
```javascript
  const sortedContributors = Array.from(contributors).sort()
  if (sortedContributors.length > 1) {
    return (
      sortedContributors.slice(0, sortedContributors.length - 1).join(', ') +
      ' and ' +
      sortedContributors.slice(-1)
    )
  } else {
    return sortedContributors[0]
  }
```

I suppose it ought to be re-written into something like this:
```javascript
  } else if (sortedContributors.length == 1) {
    return sortedContributors[0]
  } else {
    // What here?
  }
```
but then, what of the remaining branch? Should I also add a new configuration option called something like `no-contributors-template` that can be used here?
ThomasKasene: I wrote the code for a `no-contributors-template` option too, but I'm waiting for feedback from @jetersen or somebody else before I commit it to this branch, since they might want it as a separate pull request.
jetersen: could simply change it to the following

```javascript
  } else if (sortedContributors.length == 1) {
    return sortedContributors[0]
  }
```

The method does not require to return any value.
ThomasKasene: Interesting. I suppose that's another piece of weirdness of JavaScript. In that case, it is certainly an option, but what will the result be if the function doesn't return anything? If it's an empty String, then I think I would still like to be able to customize the message. Consider the following `template`:

```
template: |
  ## Contributors
  $CONTRIBUTORS
```

If all the contributors are filtered away, the release notes would contain the "Contributors" header, but nothing underneath it, which would look a little awkward.

But if you wish, I can add your suggestion to this branch, and submit a separate pull request for a `no-contributors-template` option?
jetersen: Perhaps move all the header, filtering and sorting together and simply check if length is zero after contributors are filtered. Feel free to push to this PR otherwise you are making it hard for me to review ☺️
ThomasKasene: Maybe we're misunderstanding one another, but I was talking about the `template` option, not a `contributors-template` (which currently doesn't exist). Maybe I could've been more clear and written a complete example:
```
exclude-contributors:
  - dependabot

template: |
  $CHANGES

  ## Contributors
  $CONTRIBUTORS
```

If a release is being made that only contains changes by `dependabot`, then the last part of the release notes would contain the "Contributors" header, but nothing else below it.

Given the same changes by `dependabot`, but with the following configuration, then at least there would be a little message under the header so it wouldn't look so empty:
```
exclude-contributors:
  - 'dependabot'

no-contributors-template: 'No third-party contributions this release.'

template: |
  $CHANGES

  ## Contributors
  $CONTRIBUTORS
```

I think these two options (`exclude-contributors` and `no-contributors-template`) are two that can be reviewed separately because they don't really affect one another (apart from the fact that they both add `config` to the signature of `contributorsSentence`), but it's up to you! 😃 
jetersen: @ThomasKasene could you try and build with yarn to reduce the changes in dist folder?

`yarn prettier && yarn lint --fix && yarn build`
ThomasKasene: @jetersen Absolutely! I'll take a look at it soon.

Do you want me to undo the spacing on the "Configuration Options" table in README.md as well? My lines ended up being a bit long and so I decided to expand the "width" of the other rows as well, but I realize now that it might cause unnecessary merge conflicts.
jetersen: @ThomasKasene the linter will take care of it.
ThomasKasene: Okay, nothing happened when I ran your suggested commands. I guess that means I either did it correctly by hand, or that the linter wasn't applied. Either way, dist/index.js got a little better now.
jetersen: `git merge master` & `yarn install` is important :)

Anyhow ended up doing it for ya

Patch:
diff --git a/README.md b/README.md
index c20f491..ceb8e28 100644
--- a/README.md
+++ b/README.md
@@ -103,27 +103,29 @@ template: |
 
 You can configure Release Drafter using the following key in your `.github/release-drafter.yml` file:
 
-| Key                    | Required | Description                                                                                                                                                                |
-| ---------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| `template`             | Required | The template for the body of the draft release. Use [template variables](#template-variables) to insert values.                                                            |
-| `category-template`    | Optional | The template to use for each category. Use [category template variables](#category-template-variables) to insert values. Default: `"## $TITLE"`.                           |
-| `name-template`        | Optional | The template for the name of the draft release. For example: `"v$NEXT_PATCH_VERSION"`.                                                                                     |
-| `tag-template`         | Optional | The template for the tag of the draft release. For example: `"v$NEXT_PATCH_VERSION"`.                                                                                      |
-| `version-template`     | Optional | The template to use when calculating the next version number for the release. Useful for projects that don't use semantic versioning. Default: `"$MAJOR.$MINOR.$PATCH"`    |
-| `change-template`      | Optional | The template to use for each merged pull request. Use [change template variables](#change-template-variables) to insert values. Default: `"* $TITLE (#$NUMBER) @$AUTHOR"`. |
-| `change-title-escapes` | Optional | Characters to escape in `$TITLE` when inserting into `change-template` so that they are not interpreted as Markdown format characters. Default: `""`                       |
-| `no-changes-template`  | Optional | The template to use for when there’s no changes. Default: `"* No changes"`.                                                                                                |
-| `references`           | Optional | The references to listen for configuration updates to `.github/release-drafter.yml`. Refer to [References](#references) to learn more about this                           |
-| `categories`           | Optional | Categorize pull requests using labels. Refer to [Categorize Pull Requests](#categorize-pull-requests) to learn more about this option.                                     |
-| `exclude-labels`       | Optional | Exclude pull requests using labels. Refer to [Exclude Pull Requests](#exclude-pull-requests) to learn more about this option.                                              |
-| `include-labels`       | Optional | Include only the specified pull requests using labels. Refer to [Include Pull Requests](#include-pull-requests) to learn more about this option.                           |
-| `replacers`            | Optional | Search and replace content in the generated changelog body. Refer to [Replacers](#replacers) to learn more about this option.                                              |
-| `sort-by`              | Optional | Sort changelog by merged_at or title. Can be one of: `merged_at`, `title`. Default: `merged_at`.                                                                           |
-| `sort-direction`       | Optional | Sort changelog in ascending or descending order. Can be one of: `ascending`, `descending`. Default: `descending`.                                                          |
-| `prerelease`           | Optional | Mark the draft release as pre-release. Default `false`.                                                                                                                    |
-| `version-resolver`     | Optional | Adjust the `$RESOLVED_VERSION` variable using labels. Refer to [Version Resolver](#version-resolver) to learn more about this                                              |
-| `filter-by-commitish`  | Optional | Filter previous releases to consider only the target branch of the release. Default: `false`.                                                                              |
-| `commitish`            | Optional | Specify the target branch of the release. Default: the default branch of the repo.                                                                                         |
+| Key                        | Required | Description                                                                                                                                                                |
+| -------------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `template`                 | Required | The template for the body of the draft release. Use [template variables](#template-variables) to insert values.                                                            |
+| `category-template`        | Optional | The template to use for each category. Use [category template variables](#category-template-variables) to insert values. Default: `"## $TITLE"`.                           |
+| `name-template`            | Optional | The template for the name of the draft release. For example: `"v$NEXT_PATCH_VERSION"`.                                                                                     |
+| `tag-template`             | Optional | The template for the tag of the draft release. For example: `"v$NEXT_PATCH_VERSION"`.                                                                                      |
+| `version-template`         | Optional | The template to use when calculating the next version number for the release. Useful for projects that don't use semantic versioning. Default: `"$MAJOR.$MINOR.$PATCH"`    |
+| `change-template`          | Optional | The template to use for each merged pull request. Use [change template variables](#change-template-variables) to insert values. Default: `"* $TITLE (#$NUMBER) @$AUTHOR"`. |
+| `change-title-escapes`     | Optional | Characters to escape in `$TITLE` when inserting into `change-template` so that they are not interpreted as Markdown format characters. Default: `""`                       |
+| `no-changes-template`      | Optional | The template to use for when there’s no changes. Default: `"* No changes"`.                                                                                                |
+| `references`               | Optional | The references to listen for configuration updates to `.github/release-drafter.yml`. Refer to [References](#references) to learn more about this                           |
+| `categories`               | Optional | Categorize pull requests using labels. Refer to [Categorize Pull Requests](#categorize-pull-requests) to learn more about this option.                                     |
+| `exclude-labels`           | Optional | Exclude pull requests using labels. Refer to [Exclude Pull Requests](#exclude-pull-requests) to learn more about this option.                                              |
+| `include-labels`           | Optional | Include only the specified pull requests using labels. Refer to [Include Pull Requests](#include-pull-requests) to learn more about this option.                           |
+| `exclude-contributors`     | Optional | Exclude specific usernames from the generated `$CONTRIBUTORS` variable. Refer to [Exclude Contributors](#exclude-contributors) to learn more about this option.            |
+| `no-contributors-template` | Optional | The template to use for `$CONTRIBUTORS` when there's no contributors to list. Default: `"No contributors"`.                                                                |
+| `replacers`                | Optional | Search and replace content in the generated changelog body. Refer to [Replacers](#replacers) to learn more about this option.                                              |
+| `sort-by`                  | Optional | Sort changelog by merged_at or title. Can be one of: `merged_at`, `title`. Default: `merged_at`.                                                                           |
+| `sort-direction`           | Optional | Sort changelog in ascending or descending order. Can be one of: `ascending`, `descending`. Default: `descending`.                                                          |
+| `prerelease`               | Optional | Mark the draft release as pre-release. Default `false`.                                                                                                                    |
+| `version-resolver`         | Optional | Adjust the `$RESOLVED_VERSION` variable using labels. Refer to [Version Resolver](#version-resolver) to learn more about this                                              |
+| `filter-by-commitish`      | Optional | Filter previous releases to consider only the target branch of the release. Default: `false`.                                                                              |
+| `commitish`                | Optional | Specify the target branch of the release. Default: the default branch of the repo.                                                                                         |
 
 Release Drafter also supports [Probot Config](https://github.com/probot/probot-config), if you want to store your configuration files in a central repository. This allows you to share configurations between projects, and create a organization-wide configuration file by creating a repository named `.github` with the file `.github/release-drafter.yml`.
 
@@ -261,6 +263,15 @@ include-labels:
 
 Pull requests with the label "app-foo" will be the only pull requests included in the release draft.
 
+## Exclude Contributors
+
+By default, the `$CONTRIBUTORS` variable will contain the names or usernames of all the contributors of a release. The `exclude-contributors` option allows you to remove certain usernames from that list. This can be useful if don't wish to include yourself, to better highlight only the third-party contributions.
+
+```yml
+exclude-contributors:
+  - 'myusername'
+```
+
 ## Replacers
 
 You can search and replace content in the generated changelog body, using regular expressions, with the `replacers` option. Each replacer is applied in order.
@@ -314,7 +325,7 @@ The Release Drafter GitHub Action accepts a number of optional inputs directly i
 | `tag`         | The tag name to be associated with the GitHub release that's created or updated. This will override any `tag-template` specified in your `release-drafter.yml` if defined.                                                                                                                                                                                         |
 | `version`     | The version to be associated with the GitHub release that's created or updated. This will override any version calculated by the release-drafter.                                                                                                                                                                                                                  |
 | `publish`     | A boolean indicating whether the release being created or updated should be immediately published. This may be useful if the output of a previous workflow step determines that a new version of your project has been (or will be) released, as with [`salsify/action-detect-and-tag-new-version`](https://github.com/salsify/action-detect-and-tag-new-version). |
-| `prerelease`  | A boolean indicating whether the release being created or updated is a prerelease.                                                                                                                                                                                                                                                                                  |
+| `prerelease`  | A boolean indicating whether the release being created or updated is a prerelease.                                                                                                                                                                                                                                                                                 |
 | `commitish`   | A string specifying the target branch for the release being created.                                                                                                                                                                                                                                                                                               |
 
 ## Action Outputs
diff --git a/dist/index.js b/dist/index.js
index c5ef743..b748a8e 100644
--- a/dist/index.js
+++ b/dist/index.js
@@ -478,6 +478,8 @@ const DEFAULT_CONFIG = Object.freeze({
   categories: [],
   'exclude-labels': [],
   'include-labels': [],
+  'exclude-contributors': [],
+  'no-contributors-template': 'No contributors',
   replacers: [],
   autolabeler: [],
   'sort-by': SORT_BY.mergedAt,
@@ -627,19 +629,26 @@ module.exports.findReleases = async ({ ref, context, config }) => {
   return { draftRelease, lastRelease }
 }
 
-const contributorsSentence = ({ commits, pullRequests }) => {
+const contributorsSentence = ({ commits, pullRequests, config }) => {
+  const { 'exclude-contributors': excludeContributors } = config
+
   const contributors = new Set()
 
   commits.forEach((commit) => {
     if (commit.author.user) {
-      contributors.add(`@${commit.author.user.login}`)
+      if (!excludeContributors.includes(commit.author.user.login)) {
+        contributors.add(`@${commit.author.user.login}`)
+      }
     } else {
       contributors.add(commit.author.name)
     }
   })
 
   pullRequests.forEach((pullRequest) => {
-    if (pullRequest.author) {
+    if (
+      pullRequest.author &&
+      !excludeContributors.includes(pullRequest.author.login)
+    ) {
       contributors.add(`@${pullRequest.author.login}`)
     }
   })
@@ -651,8 +660,10 @@ const contributorsSentence = ({ commits, pullRequests }) => {
       ' and ' +
       sortedContributors.slice(-1)
     )
-  } else {
+  } else if (sortedContributors.length === 1) {
     return sortedContributors[0]
+  } else {
+    return config['no-contributors-template']
   }
 }
 
@@ -826,7 +837,7 @@ module.exports.generateReleaseInfo = ({
   name = undefined,
   isPreRelease,
   shouldDraft,
-  commitish = undefined
+  commitish = undefined,
 }) => {
   let body = config.template
 
@@ -838,6 +849,7 @@ module.exports.generateReleaseInfo = ({
       $CONTRIBUTORS: contributorsSentence({
         commits,
         pullRequests: mergedPullRequests,
+        config,
       }),
     },
     config.replacers
@@ -978,6 +990,14 @@ const schema = (context) => {
         .items(Joi.string())
         .default(DEFAULT_CONFIG['include-labels']),
 
+      'exclude-contributors': Joi.array()
+        .items(Joi.string())
+        .default(DEFAULT_CONFIG['exclude-contributors']),
+
+      'no-contributors-template': Joi.string().default(
+        DEFAULT_CONFIG['no-contributors-template']
+      ),
+
       'sort-by': Joi.string()
         .valid(SORT_BY.mergedAt, SORT_BY.title)
         .default(DEFAULT_CONFIG['sort-by']),
diff --git a/lib/default-config.js b/lib/default-config.js
index d514f31..a8e98b3 100644
--- a/lib/default-config.js
+++ b/lib/default-config.js
@@ -16,6 +16,8 @@ const DEFAULT_CONFIG = Object.freeze({
   categories: [],
   'exclude-labels': [],
   'include-labels': [],
+  'exclude-contributors': [],
+  'no-contributors-template': 'No contributors',
   replacers: [],
   autolabeler: [],
   'sort-by': SORT_BY.mergedAt,
diff --git a/lib/releases.js b/lib/releases.js
index e5115ae..891f6aa 100644
--- a/lib/releases.js
+++ b/lib/releases.js
@@ -54,19 +54,26 @@ module.exports.findReleases = async ({ ref, context, config }) => {
   return { draftRelease, lastRelease }
 }
 
-const contributorsSentence = ({ commits, pullRequests }) => {
+const contributorsSentence = ({ commits, pullRequests, config }) => {
+  const { 'exclude-contributors': excludeContributors } = config
+
   const contributors = new Set()
 
   commits.forEach((commit) => {
     if (commit.author.user) {
-      contributors.add(`@${commit.author.user.login}`)
+      if (!excludeContributors.includes(commit.author.user.login)) {
+        contributors.add(`@${commit.author.user.login}`)
+      }
     } else {
       contributors.add(commit.author.name)
     }
   })
 
   pullRequests.forEach((pullRequest) => {
-    if (pullRequest.author) {
+    if (
+      pullRequest.author &&
+      !excludeContributors.includes(pullRequest.author.login)
+    ) {
       contributors.add(`@${pullRequest.author.login}`)
     }
   })
@@ -78,8 +85,10 @@ const contributorsSentence = ({ commits, pullRequests }) => {
       ' and ' +
       sortedContributors.slice(-1)
     )
-  } else {
+  } else if (sortedContributors.length === 1) {
     return sortedContributors[0]
+  } else {
+    return config['no-contributors-template']
   }
 }
 
@@ -265,6 +274,7 @@ module.exports.generateReleaseInfo = ({
       $CONTRIBUTORS: contributorsSentence({
         commits,
         pullRequests: mergedPullRequests,
+        config,
       }),
     },
     config.replacers
diff --git a/lib/schema.js b/lib/schema.js
index 53dd2c1..8ce8128 100644
--- a/lib/schema.js
+++ b/lib/schema.js
@@ -46,6 +46,14 @@ const schema = (context) => {
         .items(Joi.string())
         .default(DEFAULT_CONFIG['include-labels']),
 
+      'exclude-contributors': Joi.array()
+        .items(Joi.string())
+        .default(DEFAULT_CONFIG['exclude-contributors']),
+
+      'no-contributors-template': Joi.string().default(
+        DEFAULT_CONFIG['no-contributors-template']
+      ),
+
       'sort-by': Joi.string()
         .valid(SORT_BY.mergedAt, SORT_BY.title)
         .default(DEFAULT_CONFIG['sort-by']),
diff --git a/schema.json b/schema.json
index a2ea013..19327fa 100644
--- a/schema.json
+++ b/schema.json
@@ -73,6 +73,17 @@
         "type": "string"
       }
     },
+    "exclude-contributors": {
+      "default": [],
+      "type": "array",
+      "items": {
+        "type": "string"
+      }
+    },
+    "no-contributors-template": {
+      "default": "No contributors",
+      "type": "string"
+    },
     "sort-by": {
       "default": "merged_at",
       "enum": ["merged_at", "title"],


Test patch:
diff --git a/test/fixtures/config/config-with-contributors.yml b/test/fixtures/config/config-with-contributors.yml
index 4676aa8..e45e8b9 100644
--- a/test/fixtures/config/config-with-contributors.yml
+++ b/test/fixtures/config/config-with-contributors.yml
@@ -1,1 +1,2 @@
 template: 'A big thanks to: $CONTRIBUTORS'
+no-contributors-template: 'Nobody'
diff --git a/test/fixtures/config/config-with-exclude-contributors.yml b/test/fixtures/config/config-with-exclude-contributors.yml
new file mode 100644
index 0000000..09af10b
--- /dev/null
+++ b/test/fixtures/config/config-with-exclude-contributors.yml
@@ -0,0 +1,4 @@
+exclude-contributors:
+  - TimonVS
+  - Ada Lovelace
+template: 'A big thanks to: $CONTRIBUTORS'
diff --git a/test/index.test.js b/test/index.test.js
index 3771ea2..5f7dad8 100644
--- a/test/index.test.js
+++ b/test/index.test.js
@@ -648,6 +648,95 @@ describe('release-drafter', () => {
 
           expect.assertions(1)
         })
+
+        it('uses no-contributors-template when there are no contributors', async () => {
+          getConfigMock('config-with-contributors.yml')
+
+          nock('https://api.github.com')
+            .get(
+              '/repos/toolmantim/release-drafter-test-project/releases?per_page=100'
+            )
+            .reply(200, [require('./fixtures/release')])
+
+          nock('https://api.github.com')
+            .post('/graphql', (body) =>
+              body.query.includes('query findCommitsWithAssociatedPullRequests')
+            )
+            .reply(200, require('./fixtures/graphql-commits-empty.json'))
+
+          nock('https://api.github.com')
+            .post(
+              '/repos/toolmantim/release-drafter-test-project/releases',
+              (body) => {
+                expect(body).toMatchInlineSnapshot(`
+                  Object {
+                    "body": "A big thanks to: Nobody",
+                    "draft": true,
+                    "name": "",
+                    "prerelease": false,
+                    "tag_name": "",
+                    "target_commitish": "",
+                  }
+                `)
+                return true
+              }
+            )
+            .reply(200, require('./fixtures/release'))
+
+          await probot.receive({
+            name: 'push',
+            payload: require('./fixtures/push'),
+          })
+
+          expect.assertions(1)
+        })
+      })
+
+      describe('with exclude-contributors config', () => {
+        it('excludes matching contributors by username', async () => {
+          getConfigMock('config-with-exclude-contributors.yml')
+
+          nock('https://api.github.com')
+            .get(
+              '/repos/toolmantim/release-drafter-test-project/releases?per_page=100'
+            )
+            .reply(200, [require('./fixtures/release')])
+
+          nock('https://api.github.com')
+            .post('/graphql', (body) =>
+              body.query.includes('query findCommitsWithAssociatedPullRequests')
+            )
+            .reply(
+              200,
+              require('./fixtures/__generated__/graphql-commits-merge-commit.json')
+            )
+
+          nock('https://api.github.com')
+            .post(
+              '/repos/toolmantim/release-drafter-test-project/releases',
+              (body) => {
+                expect(body).toMatchInlineSnapshot(`
+                  Object {
+                    "body": "A big thanks to: Ada Lovelace",
+                    "draft": true,
+                    "name": "",
+                    "prerelease": false,
+                    "tag_name": "",
+                    "target_commitish": "",
+                  }
+                `)
+                return true
+              }
+            )
+            .reply(200, require('./fixtures/release'))
+
+          await probot.receive({
+            name: 'push',
+            payload: require('./fixtures/push'),
+          })
+
+          expect.assertions(1)
+        })
       })
     })
 
@@ -2200,7 +2289,7 @@ describe('release-drafter', () => {
 
                 ## Contributors
 
-                $CONTRIBUTORS
+                No contributors
 
                 ## Previous release
 
@@ -2310,7 +2399,7 @@ describe('release-drafter', () => {
 
                 ## Contributors
 
-                $CONTRIBUTORS
+                No contributors
 
                 ## Previous release