Repository: release-drafter/release-drafter
Pull request #1084 (2022-02-25 03:14:17)

include-paths: Permit filtering only PRs that modify path prefixes
#### This PR implements part of #935

---

This introduces a new GraphQL query, requesting all node IDs that contain modifications to the specified file list. That node list is then used to filter the full set of PRs to only the set that contain those node IDs, thus giving us only the PRs that include changes to our paths.

Examples of `include-paths`:

```yaml
- src/main
- README.md
- doc/
```

For a project with ancillary files that need not be considered for a release (an automated job that regenerates binary files on an automated basis, generating lots of noise, for example).

In my case, this is because I have many parallel instances of release-drafter, each running with different configurations, to release different binary artifacts from a single source tree. The project is organized this way for ease of contribution, as well as reducing needless version bumps for consumers (where different source trees contain completely unrelated changesets).


Patch:
diff --git a/bin/generate-fixtures.js b/bin/generate-fixtures.js
index e179cfb..5d45e6f 100755
--- a/bin/generate-fixtures.js
+++ b/bin/generate-fixtures.js
@@ -3,7 +3,10 @@
 const fs = require('fs')
 const path = require('path')
 const fetch = require('node-fetch')
-const { findCommitsWithAssociatedPullRequestsQuery } = require('../lib/commits')
+const {
+  findCommitsWithAssociatedPullRequestsQuery,
+  findCommitsWithPathChangesQuery,
+} = require('../lib/commits')
 
 const REPO_NAME = 'release-drafter-test-repo'
 const GITHUB_GRAPHQL_API_ENDPOINT = 'https://api.github.com/graphql'
@@ -38,25 +41,14 @@ const repos = [
   },
 ]
 
-for (const repo of repos) {
+const runQuery = (kind, repo, body) => {
   const options = {
+    body,
     method: 'POST',
     headers: {
       'Content-Type': 'application/json',
       Authorization: `bearer ${GITHUB_TOKEN}`,
     },
-    body: JSON.stringify({
-      query: findCommitsWithAssociatedPullRequestsQuery,
-      variables: {
-        owner: repo.owner,
-        name: REPO_NAME,
-        ref: repo.branch,
-        withPullRequestBody: true,
-        withPullRequestURL: true,
-        withBaseRefName: true,
-        withHeadRefName: true,
-      },
-    }),
   }
 
   fetch(GITHUB_GRAPHQL_API_ENDPOINT, options)
@@ -72,10 +64,65 @@ for (const repo of repos) {
         path.resolve(
           __dirname,
           '../test/fixtures/__generated__',
-          `graphql-commits-${repo.branch}.json`
+          `graphql-${kind}-${repo.branch}.json`
         ),
         string + '\n'
       )
     })
     .catch(console.error)
 }
+
+for (const repo of repos) {
+  runQuery(
+    'commits',
+    repo,
+    JSON.stringify({
+      query: findCommitsWithAssociatedPullRequestsQuery,
+      variables: {
+        owner: repo.owner,
+        name: REPO_NAME,
+        targetCommitish: repo.branch,
+        withPullRequestBody: true,
+        withPullRequestURL: true,
+        withBaseRefName: true,
+        withHeadRefName: true,
+      },
+    })
+  )
+
+  runQuery(
+    'include-null-path',
+    repo,
+    JSON.stringify({
+      query: findCommitsWithPathChangesQuery,
+      variables: {
+        owner: repo.owner,
+        name: REPO_NAME,
+        targetCommitish: repo.branch,
+        withPullRequestBody: true,
+        withPullRequestURL: true,
+        withBaseRefName: true,
+        withHeadRefName: true,
+        path: null,
+      },
+    })
+  )
+
+  runQuery(
+    'include-path-src-5.md',
+    repo,
+    JSON.stringify({
+      query: findCommitsWithPathChangesQuery,
+      variables: {
+        owner: repo.owner,
+        name: REPO_NAME,
+        targetCommitish: repo.branch,
+        withPullRequestBody: true,
+        withPullRequestURL: true,
+        withBaseRefName: true,
+        withHeadRefName: true,
+        path: 'src/5.md',
+      },
+    })
+  )
+}
diff --git a/dist/index.js b/dist/index.js
index eed174c..083d74f 100644
--- a/dist/index.js
+++ b/dist/index.js
@@ -128725,6 +128725,33 @@ const _ = __nccwpck_require__(90250)
 const { log } = __nccwpck_require__(71911)
 const { paginate } = __nccwpck_require__(46418)
 
+const findCommitsWithPathChangesQuery = /* GraphQL */ `
+  query findCommitsWithPathChangesQuery(
+    $name: String!
+    $owner: String!
+    $targetCommitish: String!
+    $since: GitTimestamp
+    $after: String
+    $path: String
+  ) {
+    repository(name: $name, owner: $owner) {
+      object(expression: $targetCommitish) {
+        ... on Commit {
+          history(path: $path, since: $since, after: $after) {
+            pageInfo {
+              hasNextPage
+              endCursor
+            }
+            nodes {
+              id
+            }
+          }
+        }
+      }
+    }
+  }
+`
+
 const findCommitsWithAssociatedPullRequestsQuery = /* GraphQL */ `
   query findCommitsWithAssociatedPullRequests(
     $name: String!
@@ -128804,10 +128831,40 @@ const findCommitsWithAssociatedPullRequests = async ({
     withBaseRefName: config['change-template'].includes('$BASE_REF_NAME'),
     withHeadRefName: config['change-template'].includes('$HEAD_REF_NAME'),
   }
+  const includePaths = config['include-paths']
   const dataPath = ['repository', 'object', 'history']
   const repoNameWithOwner = `${owner}/${repo}`
 
-  let data, commits
+  let data,
+    allCommits,
+    includedIds = {}
+
+  if (includePaths.length > 0) {
+    var anyChanges = false
+    for (const path of includePaths) {
+      const pathData = await paginate(
+        context.octokit.graphql,
+        findCommitsWithPathChangesQuery,
+        lastRelease
+          ? { ...variables, since: lastRelease.created_at, path }
+          : { ...variables, path },
+        dataPath
+      )
+      const commitsWithPathChanges = _.get(pathData, [...dataPath, 'nodes'])
+
+      includedIds[path] = includedIds[path] || new Set([])
+      for (const { id } of commitsWithPathChanges) {
+        anyChanges = true
+        includedIds[path].add(id)
+      }
+    }
+
+    if (!anyChanges) {
+      // Short circuit to avoid blowing GraphQL budget
+      return { commits: [], pullRequests: [] }
+    }
+  }
+
   if (lastRelease) {
     log({
       context,
@@ -128822,7 +128879,7 @@ const findCommitsWithAssociatedPullRequests = async ({
     )
     // GraphQL call is inclusive of commits from the specified dates.  This means the final
     // commit from the last tag is included, so we remove this here.
-    commits = _.get(data, [...dataPath, 'nodes']).filter(
+    allCommits = _.get(data, [...dataPath, 'nodes']).filter(
       (commit) => commit.committedDate != lastRelease.created_at
     )
   } else {
@@ -128834,9 +128891,16 @@ const findCommitsWithAssociatedPullRequests = async ({
       variables,
       dataPath
     )
-    commits = _.get(data, [...dataPath, 'nodes'])
+    allCommits = _.get(data, [...dataPath, 'nodes'])
   }
 
+  const commits =
+    includePaths.length > 0
+      ? allCommits.filter((commit) =>
+          includePaths.some((path) => includedIds[path].has(commit.id))
+        )
+      : allCommits
+
   const pullRequests = _.uniqBy(
     commits.flatMap((commit) => commit.associatedPullRequests.nodes),
     'number'
@@ -128850,6 +128914,8 @@ const findCommitsWithAssociatedPullRequests = async ({
 exports.findCommitsWithAssociatedPullRequestsQuery =
   findCommitsWithAssociatedPullRequestsQuery
 
+exports.findCommitsWithPathChangesQuery = findCommitsWithPathChangesQuery
+
 exports.findCommitsWithAssociatedPullRequests =
   findCommitsWithAssociatedPullRequests
 
@@ -128944,6 +129010,7 @@ const DEFAULT_CONFIG = Object.freeze({
   categories: [],
   'exclude-labels': [],
   'include-labels': [],
+  'include-paths': [],
   'exclude-contributors': [],
   'no-contributors-template': 'No contributors',
   replacers: [],
@@ -129514,6 +129581,10 @@ const schema = (context) => {
         .items(Joi.string())
         .default(DEFAULT_CONFIG['include-labels']),
 
+      'include-paths': Joi.array()
+        .items(Joi.string())
+        .default(DEFAULT_CONFIG['include-paths']),
+
       'exclude-contributors': Joi.array()
         .items(Joi.string())
         .default(DEFAULT_CONFIG['exclude-contributors']),
diff --git a/lib/commits.js b/lib/commits.js
index b42fc30..cfbe8b9 100644
--- a/lib/commits.js
+++ b/lib/commits.js
@@ -2,6 +2,33 @@ const _ = require('lodash')
 const { log } = require('./log')
 const { paginate } = require('./pagination')
 
+const findCommitsWithPathChangesQuery = /* GraphQL */ `
+  query findCommitsWithPathChangesQuery(
+    $name: String!
+    $owner: String!
+    $targetCommitish: String!
+    $since: GitTimestamp
+    $after: String
+    $path: String
+  ) {
+    repository(name: $name, owner: $owner) {
+      object(expression: $targetCommitish) {
+        ... on Commit {
+          history(path: $path, since: $since, after: $after) {
+            pageInfo {
+              hasNextPage
+              endCursor
+            }
+            nodes {
+              id
+            }
+          }
+        }
+      }
+    }
+  }
+`
+
 const findCommitsWithAssociatedPullRequestsQuery = /* GraphQL */ `
   query findCommitsWithAssociatedPullRequests(
     $name: String!
@@ -81,10 +108,40 @@ const findCommitsWithAssociatedPullRequests = async ({
     withBaseRefName: config['change-template'].includes('$BASE_REF_NAME'),
     withHeadRefName: config['change-template'].includes('$HEAD_REF_NAME'),
   }
+  const includePaths = config['include-paths']
   const dataPath = ['repository', 'object', 'history']
   const repoNameWithOwner = `${owner}/${repo}`
 
-  let data, commits
+  let data,
+    allCommits,
+    includedIds = {}
+
+  if (includePaths.length > 0) {
+    var anyChanges = false
+    for (const path of includePaths) {
+      const pathData = await paginate(
+        context.octokit.graphql,
+        findCommitsWithPathChangesQuery,
+        lastRelease
+          ? { ...variables, since: lastRelease.created_at, path }
+          : { ...variables, path },
+        dataPath
+      )
+      const commitsWithPathChanges = _.get(pathData, [...dataPath, 'nodes'])
+
+      includedIds[path] = includedIds[path] || new Set([])
+      for (const { id } of commitsWithPathChanges) {
+        anyChanges = true
+        includedIds[path].add(id)
+      }
+    }
+
+    if (!anyChanges) {
+      // Short circuit to avoid blowing GraphQL budget
+      return { commits: [], pullRequests: [] }
+    }
+  }
+
   if (lastRelease) {
     log({
       context,
@@ -99,7 +156,7 @@ const findCommitsWithAssociatedPullRequests = async ({
     )
     // GraphQL call is inclusive of commits from the specified dates.  This means the final
     // commit from the last tag is included, so we remove this here.
-    commits = _.get(data, [...dataPath, 'nodes']).filter(
+    allCommits = _.get(data, [...dataPath, 'nodes']).filter(
       (commit) => commit.committedDate != lastRelease.created_at
     )
   } else {
@@ -111,9 +168,16 @@ const findCommitsWithAssociatedPullRequests = async ({
       variables,
       dataPath
     )
-    commits = _.get(data, [...dataPath, 'nodes'])
+    allCommits = _.get(data, [...dataPath, 'nodes'])
   }
 
+  const commits =
+    includePaths.length > 0
+      ? allCommits.filter((commit) =>
+          includePaths.some((path) => includedIds[path].has(commit.id))
+        )
+      : allCommits
+
   const pullRequests = _.uniqBy(
     commits.flatMap((commit) => commit.associatedPullRequests.nodes),
     'number'
@@ -127,5 +191,7 @@ const findCommitsWithAssociatedPullRequests = async ({
 exports.findCommitsWithAssociatedPullRequestsQuery =
   findCommitsWithAssociatedPullRequestsQuery
 
+exports.findCommitsWithPathChangesQuery = findCommitsWithPathChangesQuery
+
 exports.findCommitsWithAssociatedPullRequests =
   findCommitsWithAssociatedPullRequests
diff --git a/lib/default-config.js b/lib/default-config.js
index 69b9a00..e3d8495 100644
--- a/lib/default-config.js
+++ b/lib/default-config.js
@@ -16,6 +16,7 @@ const DEFAULT_CONFIG = Object.freeze({
   categories: [],
   'exclude-labels': [],
   'include-labels': [],
+  'include-paths': [],
   'exclude-contributors': [],
   'no-contributors-template': 'No contributors',
   replacers: [],
diff --git a/lib/schema.js b/lib/schema.js
index a4368cb..521ba1b 100644
--- a/lib/schema.js
+++ b/lib/schema.js
@@ -51,6 +51,10 @@ const schema = (context) => {
         .items(Joi.string())
         .default(DEFAULT_CONFIG['include-labels']),
 
+      'include-paths': Joi.array()
+        .items(Joi.string())
+        .default(DEFAULT_CONFIG['include-paths']),
+
       'exclude-contributors': Joi.array()
         .items(Joi.string())
         .default(DEFAULT_CONFIG['exclude-contributors']),


Test patch:
diff --git a/test/fixtures/__generated__/README.md b/test/fixtures/__generated__/README.md
deleted file mode 100644
index 049e74c..0000000
--- a/test/fixtures/__generated__/README.md
+++ /dev/null
@@ -1,1 +0,0 @@
-These fixtures are generated using the `bin/generate-fixtures.js` script and should be regenerated when the query in `lib/commits.js` changes. The fixtures are generated from https://github.com/TimonVS/release-drafter-test-repo.
diff --git a/test/fixtures/__generated__/graphql-include-null-path-forking.json b/test/fixtures/__generated__/graphql-include-null-path-forking.json
new file mode 100644
index 0000000..dfc21c6
--- /dev/null
+++ b/test/fixtures/__generated__/graphql-include-null-path-forking.json
@@ -0,0 +1,58 @@
+{
+  "data": {
+    "repository": {
+      "object": {
+        "history": {
+          "pageInfo": {
+            "hasNextPage": false,
+            "endCursor": "96cf76bb3470913a449d505f2011544b661652f4 13"
+          },
+          "nodes": [
+            {
+              "id": "MDY6Q29tbWl0MjY3MTQyMTYxOjk2Y2Y3NmJiMzQ3MDkxM2E0NDlkNTA1ZjIwMTE1NDRiNjYxNjUyZjQ="
+            },
+            {
+              "id": "MDY6Q29tbWl0MjY3MTQyMTYxOjlhZjJlZWMyYjY4NDA0MWY5ZDE3YmFlM2IwNTAwMjYwNGU5NGE4MzM="
+            },
+            {
+              "id": "MDY6Q29tbWl0MjY3MTQyMTYxOjYxMTAwMDY2OWJmZTRlYjQ2NjUzYTQ0YjU2MGUzNjFkYjRhZDc5MjQ="
+            },
+            {
+              "id": "MDY6Q29tbWl0MjY3MTQyMTYxOjIyOWY2MWY0ODNmNDEzODFjODUwMThkNmZmMGIwZjMyYzk0NWNhMTI="
+            },
+            {
+              "id": "MDY6Q29tbWl0MjY3MTQyMTYxOmMzYThhNDY4NmFlMDkwNzdmNDk0YzcyOGJhMzc5ZTdhMDU4ZWJiYzY="
+            },
+            {
+              "id": "MDY6Q29tbWl0MjY3MTQyMTYxOmMwOWM2NmZkNWU2YjU2NTE3MzRlMTI1NGY4YjkzZTZkOWI1ZjI5ZmQ="
+            },
+            {
+              "id": "MDY6Q29tbWl0MjY3MTQyMTYxOmNjNTliYjUxN2EwOGYxNDhkZThlM2JiMDIxOTkyNjQ1YTQwMGNlNzY="
+            },
+            {
+              "id": "MDY6Q29tbWl0MjY3MTQyMTYxOjg1ODExZmJkYmE2OTViYjk3ZDc3YTVlMmE5NjNiMzFmNTM4YTgyMjM="
+            },
+            {
+              "id": "MDY6Q29tbWl0MjY3MTQyMTYxOmVkM2FmY2QzNmM4MDhlZWIxNWFhZjc0Zjg3MTNiODVkZTA2ZjBhMTk="
+            },
+            {
+              "id": "MDY6Q29tbWl0MjY3MTQyMTYxOmQyMTM0MjNmOTBjZjU5Y2YxMmRiNmUwMTIxYjgwNzE1NTE4NGRiNmQ="
+            },
+            {
+              "id": "MDY6Q29tbWl0MjY3MTQyMTYxOmQ5NTJjMjU1MDI5N2E4NTE1MmJlMWM4MjYwYjI1ZmI4OWYyODFkNDk="
+            },
+            {
+              "id": "MDY6Q29tbWl0MjY3MTQyMTYxOjk0ZGViYjMwZTAyYjExNTc1YWQ0YzBmMDNmMWE1ZjdmOTJiMTFiMjg="
+            },
+            {
+              "id": "MDY6Q29tbWl0MjY3MTQyMTYxOjNhYmJhNTkxOGZmN2QxMmZhYjMyMjA1N2ZiZGMyM2I1MzVlNzZkMDE="
+            },
+            {
+              "id": "MDY6Q29tbWl0MjY3MTQyMTYxOmI5YWI2MGVlNGI4ZjNmYTg0Mjg2NDQ0NjViZGI1YTg4ZDcyZTY1M2E="
+            }
+          ]
+        }
+      }
+    }
+  }
+}
diff --git a/test/fixtures/__generated__/graphql-include-null-path-merge-commit.json b/test/fixtures/__generated__/graphql-include-null-path-merge-commit.json
new file mode 100644
index 0000000..6bec31f
--- /dev/null
+++ b/test/fixtures/__generated__/graphql-include-null-path-merge-commit.json
@@ -0,0 +1,64 @@
+{
+  "data": {
+    "repository": {
+      "object": {
+        "history": {
+          "pageInfo": {
+            "hasNextPage": false,
+            "endCursor": "00de634cb5f3c0b854eb30b0afe5b1816e372ed3 15"
+          },
+          "nodes": [
+            {
+              "id": "MDY6Q29tbWl0MTgzNzY2OTUzOjAwZGU2MzRjYjVmM2MwYjg1NGViMzBiMGFmZTViMTgxNmUzNzJlZDM="
+            },
+            {
+              "id": "MDY6Q29tbWl0MTgzNzY2OTUzOmQ4NTg3ZmRiMjYzNjM5Zjc2NTYxMjQ2ZDUzN2UwNDE4YTJmODZhZWU="
+            },
+            {
+              "id": "MDY6Q29tbWl0MTgzNzY2OTUzOjI1MzQ0MDhmZWI4MzZkYWNjNTFlYzdjMGFmNGYyNDdlN2JlMzlkNzQ="
+            },
+            {
+              "id": "MDY6Q29tbWl0MTgzNzY2OTUzOjQ4ZTkwNTYwNWQ4ODkyNWI1MjJjMmIwMGY2NDZmNzQ0ZmQ5NzZkMjE="
+            },
+            {
+              "id": "MDY6Q29tbWl0MTgzNzY2OTUzOjVmNTViZThiOTA2M2IzOWQ5Y2VkYzllNWRkOGZkMDIyMjFiZDMxZjQ="
+            },
+            {
+              "id": "MDY6Q29tbWl0MTgzNzY2OTUzOjg1ODExZmJkYmE2OTViYjk3ZDc3YTVlMmE5NjNiMzFmNTM4YTgyMjM="
+            },
+            {
+              "id": "MDY6Q29tbWl0MTgzNzY2OTUzOmVkM2FmY2QzNmM4MDhlZWIxNWFhZjc0Zjg3MTNiODVkZTA2ZjBhMTk="
+            },
+            {
+              "id": "MDY6Q29tbWl0MTgzNzY2OTUzOmQyMTM0MjNmOTBjZjU5Y2YxMmRiNmUwMTIxYjgwNzE1NTE4NGRiNmQ="
+            },
+            {
+              "id": "MDY6Q29tbWl0MTgzNzY2OTUzOmQ5NTJjMjU1MDI5N2E4NTE1MmJlMWM4MjYwYjI1ZmI4OWYyODFkNDk="
+            },
+            {
+              "id": "MDY6Q29tbWl0MTgzNzY2OTUzOjFiYmJiM2FhYWFmMTk3MDYyYmRmMjg2ODhiMDk4NTI0NjcxM2NkMTI="
+            },
+            {
+              "id": "MDY6Q29tbWl0MTgzNzY2OTUzOjNlOGE5YTc4ODA3MmRiZDcyMmMxZTkzYTU0NzEzYzI5NzEyNjI5MDY="
+            },
+            {
+              "id": "MDY6Q29tbWl0MTgzNzY2OTUzOmI0YWZlNDc3YTZlMmU0OWFmYzdhYTU1ZWYwODMyOTU4MWJmODRiNDM="
+            },
+            {
+              "id": "MDY6Q29tbWl0MTgzNzY2OTUzOmQwMWJjZmNjNzczMWRlYzNiOTg2OWY5ZTFkYTYwMjlhYTY2NzcyNDA="
+            },
+            {
+              "id": "MDY6Q29tbWl0MTgzNzY2OTUzOjk0ZGViYjMwZTAyYjExNTc1YWQ0YzBmMDNmMWE1ZjdmOTJiMTFiMjg="
+            },
+            {
+              "id": "MDY6Q29tbWl0MTgzNzY2OTUzOjNhYmJhNTkxOGZmN2QxMmZhYjMyMjA1N2ZiZGMyM2I1MzVlNzZkMDE="
+            },
+            {
+              "id": "MDY6Q29tbWl0MTgzNzY2OTUzOmI5YWI2MGVlNGI4ZjNmYTg0Mjg2NDQ0NjViZGI1YTg4ZDcyZTY1M2E="
+            }
+          ]
+        }
+      }
+    }
+  }
+}
diff --git a/test/fixtures/__generated__/graphql-include-null-path-overlapping-label.json b/test/fixtures/__generated__/graphql-include-null-path-overlapping-label.json
new file mode 100644
index 0000000..e2e4ae9
--- /dev/null
+++ b/test/fixtures/__generated__/graphql-include-null-path-overlapping-label.json
@@ -0,0 +1,37 @@
+{
+  "data": {
+    "repository": {
+      "object": {
+        "history": {
+          "pageInfo": {
+            "hasNextPage": false,
+            "endCursor": "b2ed3f089aa1022e91e0a9d109ab876fde688c34 6"
+          },
+          "nodes": [
+            {
+              "id": "MDY6Q29tbWl0MTgzNzY2OTUzOmIyZWQzZjA4OWFhMTAyMmU5MWUwYTlkMTA5YWI4NzZmZGU2ODhjMzQ="
+            },
+            {
+              "id": "MDY6Q29tbWl0MTgzNzY2OTUzOmI2NzdkYjY0MGQwYjljMjViYjVmYjE3NjhiMzQ3NGEyNTI5MzQ5NzM="
+            },
+            {
+              "id": "MDY6Q29tbWl0MTgzNzY2OTUzOjQ3N2FjNTBlNGNkMWMzOTRhYmJkNjcwZjFkNTkwY2I3OWJjOThhYzk="
+            },
+            {
+              "id": "MDY6Q29tbWl0MTgzNzY2OTUzOjliYTBlNTQ4MmMwY2I1OWU0MDBmYzhmNWUyNGU1OWMyMDE2N2U5MGE="
+            },
+            {
+              "id": "MDY6Q29tbWl0MTgzNzY2OTUzOmY5YmYyMmMzNTI0MTcyODg0NTJhYmZkMzlkMGIxY2JhMmViYjk2MWE="
+            },
+            {
+              "id": "MDY6Q29tbWl0MTgzNzY2OTUzOjNhYmJhNTkxOGZmN2QxMmZhYjMyMjA1N2ZiZGMyM2I1MzVlNzZkMDE="
+            },
+            {
+              "id": "MDY6Q29tbWl0MTgzNzY2OTUzOmI5YWI2MGVlNGI4ZjNmYTg0Mjg2NDQ0NjViZGI1YTg4ZDcyZTY1M2E="
+            }
+          ]
+        }
+      }
+    }
+  }
+}
diff --git a/test/fixtures/__generated__/graphql-include-null-path-rebase-merging.json b/test/fixtures/__generated__/graphql-include-null-path-rebase-merging.json
new file mode 100644
index 0000000..5c2bdf1
--- /dev/null
+++ b/test/fixtures/__generated__/graphql-include-null-path-rebase-merging.json
@@ -0,0 +1,49 @@
+{
+  "data": {
+    "repository": {
+      "object": {
+        "history": {
+          "pageInfo": {
+            "hasNextPage": false,
+            "endCursor": "ed0a2bec8c895bb908638795c9da64763be75120 10"
+          },
+          "nodes": [
+            {
+              "id": "MDY6Q29tbWl0MTgzNzY2OTUzOmVkMGEyYmVjOGM4OTViYjkwODYzODc5NWM5ZGE2NDc2M2JlNzUxMjA="
+            },
+            {
+              "id": "MDY6Q29tbWl0MTgzNzY2OTUzOmZhNDQ2NGQ3MTIyNjg1OTY3MDNjNmI0OGNiNTI4NjkzNzE3NzRlYmY="
+            },
+            {
+              "id": "MDY6Q29tbWl0MTgzNzY2OTUzOjkxZjkzZTk1YTBhZGRlNzE0MTk4OTc2OWJlMWJhZTVmOWU2ZDQ4MDA="
+            },
+            {
+              "id": "MDY6Q29tbWl0MTgzNzY2OTUzOjk0NjY1ZWQyMWVjN2YwMTU1MzBkMmRhMWM0ODQzMTVlNTg2NmM2NzY="
+            },
+            {
+              "id": "MDY6Q29tbWl0MTgzNzY2OTUzOmMyMmY2MjI0OGYzZDAzMWRmMDAxYWRiZWRkNmY0NGZkOGM3YzQyZDY="
+            },
+            {
+              "id": "MDY6Q29tbWl0MTgzNzY2OTUzOmQ5MDRkYWFmMzFmOGZkN2M4NjI3MzlkYWNlMjM0ZGViNGQ5MTA0ZmE="
+            },
+            {
+              "id": "MDY6Q29tbWl0MTgzNzY2OTUzOjRmMmEwNjkxOTRlYWY5MDAwMWM2ZDg3Y2EwN2RhNjA0ZDM1Zjc0YTA="
+            },
+            {
+              "id": "MDY6Q29tbWl0MTgzNzY2OTUzOjJjZjU4MTc2OTZiNTExODRlNzc5MmI4ODczYWI3MTk4ODIwYmU0ZGE="
+            },
+            {
+              "id": "MDY6Q29tbWl0MTgzNzY2OTUzOjIzMmM2MjQ5MTRmMzMyZmI2MmMwZDdiYmViYmYwYWE4NGIyYjJhZWU="
+            },
+            {
+              "id": "MDY6Q29tbWl0MTgzNzY2OTUzOjNhYmJhNTkxOGZmN2QxMmZhYjMyMjA1N2ZiZGMyM2I1MzVlNzZkMDE="
+            },
+            {
+              "id": "MDY6Q29tbWl0MTgzNzY2OTUzOmI5YWI2MGVlNGI4ZjNmYTg0Mjg2NDQ0NjViZGI1YTg4ZDcyZTY1M2E="
+            }
+          ]
+        }
+      }
+    }
+  }
+}
diff --git a/test/fixtures/__generated__/graphql-include-null-path-squash-merging.json b/test/fixtures/__generated__/graphql-include-null-path-squash-merging.json
new file mode 100644
index 0000000..78e79c9
--- /dev/null
+++ b/test/fixtures/__generated__/graphql-include-null-path-squash-merging.json
@@ -0,0 +1,37 @@
+{
+  "data": {
+    "repository": {
+      "object": {
+        "history": {
+          "pageInfo": {
+            "hasNextPage": false,
+            "endCursor": "3f37d85d6f8bc9e456a93600d1648fe81316ac7c 6"
+          },
+          "nodes": [
+            {
+              "id": "MDY6Q29tbWl0MTgzNzY2OTUzOjNmMzdkODVkNmY4YmM5ZTQ1NmE5MzYwMGQxNjQ4ZmU4MTMxNmFjN2M="
+            },
+            {
+              "id": "MDY6Q29tbWl0MTgzNzY2OTUzOmViMmVjZjc4YjNjZGI0Y2VlMzc2MmZkZmQ0MjY2MmMwNWM3MzQzZTM="
+            },
+            {
+              "id": "MDY6Q29tbWl0MTgzNzY2OTUzOjc5Zjc4ODBmMTU1ZWU3ZGZkZGUxNmVlMTFmYTMwZmFhMDljODhlMjU="
+            },
+            {
+              "id": "MDY6Q29tbWl0MTgzNzY2OTUzOmUyMTZhODViOGNlMzU2MzRkN2IyMGNlMjU5YmRmZTA4ZjFhMGJiZGI="
+            },
+            {
+              "id": "MDY6Q29tbWl0MTgzNzY2OTUzOmQzNmM3YmU4OTBlOGEwYzY3NWYyNzUzMDk0YzYyNmQyN2RiOWI5NmM="
+            },
+            {
+              "id": "MDY6Q29tbWl0MTgzNzY2OTUzOjNhYmJhNTkxOGZmN2QxMmZhYjMyMjA1N2ZiZGMyM2I1MzVlNzZkMDE="
+            },
+            {
+              "id": "MDY6Q29tbWl0MTgzNzY2OTUzOmI5YWI2MGVlNGI4ZjNmYTg0Mjg2NDQ0NjViZGI1YTg4ZDcyZTY1M2E="
+            }
+          ]
+        }
+      }
+    }
+  }
+}
diff --git a/test/fixtures/__generated__/graphql-include-path-src-5.md-forking.json b/test/fixtures/__generated__/graphql-include-path-src-5.md-forking.json
new file mode 100644
index 0000000..9cf9a66
--- /dev/null
+++ b/test/fixtures/__generated__/graphql-include-path-src-5.md-forking.json
@@ -0,0 +1,22 @@
+{
+  "data": {
+    "repository": {
+      "object": {
+        "history": {
+          "pageInfo": {
+            "hasNextPage": false,
+            "endCursor": "96cf76bb3470913a449d505f2011544b661652f4 1"
+          },
+          "nodes": [
+            {
+              "id": "MDY6Q29tbWl0MjY3MTQyMTYxOjg1ODExZmJkYmE2OTViYjk3ZDc3YTVlMmE5NjNiMzFmNTM4YTgyMjM="
+            },
+            {
+              "id": "MDY6Q29tbWl0MjY3MTQyMTYxOmVkM2FmY2QzNmM4MDhlZWIxNWFhZjc0Zjg3MTNiODVkZTA2ZjBhMTk="
+            }
+          ]
+        }
+      }
+    }
+  }
+}
diff --git a/test/fixtures/__generated__/graphql-include-path-src-5.md-merge-commit.json b/test/fixtures/__generated__/graphql-include-path-src-5.md-merge-commit.json
new file mode 100644
index 0000000..dc8e24a
--- /dev/null
+++ b/test/fixtures/__generated__/graphql-include-path-src-5.md-merge-commit.json
@@ -0,0 +1,22 @@
+{
+  "data": {
+    "repository": {
+      "object": {
+        "history": {
+          "pageInfo": {
+            "hasNextPage": false,
+            "endCursor": "00de634cb5f3c0b854eb30b0afe5b1816e372ed3 1"
+          },
+          "nodes": [
+            {
+              "id": "MDY6Q29tbWl0MTgzNzY2OTUzOjg1ODExZmJkYmE2OTViYjk3ZDc3YTVlMmE5NjNiMzFmNTM4YTgyMjM="
+            },
+            {
+              "id": "MDY6Q29tbWl0MTgzNzY2OTUzOmVkM2FmY2QzNmM4MDhlZWIxNWFhZjc0Zjg3MTNiODVkZTA2ZjBhMTk="
+            }
+          ]
+        }
+      }
+    }
+  }
+}
diff --git a/test/fixtures/__generated__/graphql-include-path-src-5.md-overlapping-label.json b/test/fixtures/__generated__/graphql-include-path-src-5.md-overlapping-label.json
new file mode 100644
index 0000000..1fe9af6
--- /dev/null
+++ b/test/fixtures/__generated__/graphql-include-path-src-5.md-overlapping-label.json
@@ -0,0 +1,19 @@
+{
+  "data": {
+    "repository": {
+      "object": {
+        "history": {
+          "pageInfo": {
+            "hasNextPage": false,
+            "endCursor": "b2ed3f089aa1022e91e0a9d109ab876fde688c34 0"
+          },
+          "nodes": [
+            {
+              "id": "MDY6Q29tbWl0MTgzNzY2OTUzOmIyZWQzZjA4OWFhMTAyMmU5MWUwYTlkMTA5YWI4NzZmZGU2ODhjMzQ="
+            }
+          ]
+        }
+      }
+    }
+  }
+}
diff --git a/test/fixtures/__generated__/graphql-include-path-src-5.md-rebase-merging.json b/test/fixtures/__generated__/graphql-include-path-src-5.md-rebase-merging.json
new file mode 100644
index 0000000..0af6b00
--- /dev/null
+++ b/test/fixtures/__generated__/graphql-include-path-src-5.md-rebase-merging.json
@@ -0,0 +1,22 @@
+{
+  "data": {
+    "repository": {
+      "object": {
+        "history": {
+          "pageInfo": {
+            "hasNextPage": false,
+            "endCursor": "ed0a2bec8c895bb908638795c9da64763be75120 1"
+          },
+          "nodes": [
+            {
+              "id": "MDY6Q29tbWl0MTgzNzY2OTUzOmVkMGEyYmVjOGM4OTViYjkwODYzODc5NWM5ZGE2NDc2M2JlNzUxMjA="
+            },
+            {
+              "id": "MDY6Q29tbWl0MTgzNzY2OTUzOmZhNDQ2NGQ3MTIyNjg1OTY3MDNjNmI0OGNiNTI4NjkzNzE3NzRlYmY="
+            }
+          ]
+        }
+      }
+    }
+  }
+}
diff --git a/test/fixtures/__generated__/graphql-include-path-src-5.md-squash-merging.json b/test/fixtures/__generated__/graphql-include-path-src-5.md-squash-merging.json
new file mode 100644
index 0000000..cac6948
--- /dev/null
+++ b/test/fixtures/__generated__/graphql-include-path-src-5.md-squash-merging.json
@@ -0,0 +1,19 @@
+{
+  "data": {
+    "repository": {
+      "object": {
+        "history": {
+          "pageInfo": {
+            "hasNextPage": false,
+            "endCursor": "3f37d85d6f8bc9e456a93600d1648fe81316ac7c 0"
+          },
+          "nodes": [
+            {
+              "id": "MDY6Q29tbWl0MTgzNzY2OTUzOjNmMzdkODVkNmY4YmM5ZTQ1NmE5MzYwMGQxNjQ4ZmU4MTMxNmFjN2M="
+            }
+          ]
+        }
+      }
+    }
+  }
+}
diff --git a/test/fixtures/config/config-with-include-paths.yml b/test/fixtures/config/config-with-include-paths.yml
new file mode 100644
index 0000000..882f96f
--- /dev/null
+++ b/test/fixtures/config/config-with-include-paths.yml
@@ -0,0 +1,8 @@
+template: |
+  # What's Changed
+  $CHANGES
+name-template: 'v$INPUT_VERSION (Code name: Placeholder)'
+tag-template: v$INPUT_VERSION
+version-template: $MAJOR.$MINOR.$PATCH
+include-paths:
+  - src/5.md
diff --git a/test/index.test.js b/test/index.test.js
index 3f26714..78da5ca 100644
--- a/test/index.test.js
+++ b/test/index.test.js
@@ -13,6 +13,8 @@ const release3Payload = require('./fixtures/release-3.json')
 const pushNonMasterPayload = require('./fixtures/push-non-master-branch.json')
 const graphqlCommitsNoPRsPayload = require('./fixtures/graphql-commits-no-prs.json')
 const graphqlCommitsMergeCommit = require('./fixtures/__generated__/graphql-commits-merge-commit.json')
+const graphqlNullIncludePathMergeCommit = require('./fixtures/__generated__/graphql-include-null-path-merge-commit.json')
+const graphqlIncludePathMergeCommit = require('./fixtures/__generated__/graphql-include-path-src-5.md-merge-commit.json')
 const graphqlCommitsEmpty = require('./fixtures/graphql-commits-empty.json')
 const releaseDrafterFixture = require('./fixtures/release-draft.json')
 const graphqlCommitsOverlappingLabel = require('./fixtures/__generated__/graphql-commits-overlapping-label.json')
@@ -2118,6 +2120,112 @@ describe('release-drafter', () => {
     })
   })
 
+  describe('with include-paths config', () => {
+    it('returns all PRs when not path filtered', async () => {
+      getConfigMock('config-with-include-paths.yml')
+
+      nock('https://api.github.com')
+        .post('/graphql', (body) =>
+          body.query.includes('query findCommitsWithAssociatedPullRequests')
+        )
+        .reply(200, graphqlCommitsMergeCommit)
+        .post('/graphql', (body) =>
+          body.query.includes('query findCommitsWithPathChangesQuery')
+        )
+        .reply(200, graphqlNullIncludePathMergeCommit)
+
+      nock('https://api.github.com')
+        .get(
+          '/repos/toolmantim/release-drafter-test-project/releases?per_page=100'
+        )
+        .reply(200, [])
+
+      nock('https://api.github.com')
+        .post(
+          '/repos/toolmantim/release-drafter-test-project/releases',
+          (body) => {
+            expect(body).toMatchInlineSnapshot(`
+              Object {
+                "body": "# What's Changed
+              * Add documentation (#5) @TimonVS
+              * Update dependencies (#4) @TimonVS
+              * Bug fixes (#3) @TimonVS
+              * Add big feature (#2) @TimonVS
+              * 👽 Add alien technology (#1) @TimonVS
+              ",
+                "draft": true,
+                "name": "v$INPUT_VERSION (Code name: Placeholder)",
+                "prerelease": false,
+                "tag_name": "v$INPUT_VERSION",
+                "target_commitish": "refs/heads/master",
+              }
+            `)
+            return true
+          }
+        )
+        .reply(200, releasePayload)
+
+      const payload = pushPayload
+
+      await probot.receive({
+        name: 'push',
+        payload,
+      })
+
+      expect.assertions(1)
+    })
+
+    it('returns the modified paths', async () => {
+      getConfigMock('config-with-include-paths.yml')
+
+      nock('https://api.github.com')
+        .post('/graphql', (body) =>
+          body.query.includes('query findCommitsWithAssociatedPullRequests')
+        )
+        .reply(200, graphqlCommitsMergeCommit)
+        .post('/graphql', (body) =>
+          body.query.includes('query findCommitsWithPathChangesQuery')
+        )
+        .reply(200, graphqlIncludePathMergeCommit)
+
+      nock('https://api.github.com')
+        .get(
+          '/repos/toolmantim/release-drafter-test-project/releases?per_page=100'
+        )
+        .reply(200, [])
+
+      nock('https://api.github.com')
+        .post(
+          '/repos/toolmantim/release-drafter-test-project/releases',
+          (body) => {
+            expect(body).toMatchInlineSnapshot(`
+              Object {
+                "body": "# What's Changed
+              * Add documentation (#5) @TimonVS
+              ",
+                "draft": true,
+                "name": "v$INPUT_VERSION (Code name: Placeholder)",
+                "prerelease": false,
+                "tag_name": "v$INPUT_VERSION",
+                "target_commitish": "refs/heads/master",
+              }
+            `)
+            return true
+          }
+        )
+        .reply(200, releasePayload)
+
+      const payload = pushPayload
+
+      await probot.receive({
+        name: 'push',
+        payload,
+      })
+
+      expect.assertions(1)
+    })
+  })
+
   describe('config error handling', () => {
     it('schema error', async () => {
       getConfigMock('config-with-schema-error.yml')