Repository: intuit/auto
Pull request #890 (2020-01-24 19:44:00)

determine first time contributor by using PR count instead of git history
# What Changed

Previously we tried to determine first time contributors (FTC) by looking at the git history. This was flaky and didn't work most of the time. Now we just look at how many PRs have been made by a user.

# Why

It was broke.

Todo:

- [x] Add tests

<!-- GITHUB_RELEASE PR BODY: canary-version -->
Published PR with canary version: <details><summary>Published under canary scope @auto-canary</summary>
- `@auto-canary/auto@9.2.3-canary.890.11643.0`
- `@auto-canary/core@9.2.3-canary.890.11643.0`
- `@auto-canary/all-contributors@9.2.3-canary.890.11643.0`
- `@auto-canary/chrome@9.2.3-canary.890.11643.0`
- `@auto-canary/conventional-commits@9.2.3-canary.890.11643.0`
- `@auto-canary/crates@9.2.3-canary.890.11643.0`
- `@auto-canary/first-time-contributor@9.2.3-canary.890.11643.0`
- `@auto-canary/git-tag@9.2.3-canary.890.11643.0`
- `@auto-canary/jira@9.2.3-canary.890.11643.0`
- `@auto-canary/maven@9.2.3-canary.890.11643.0`
- `@auto-canary/npm@9.2.3-canary.890.11643.0`
- `@auto-canary/omit-commits@9.2.3-canary.890.11643.0`
- `@auto-canary/omit-release-notes@9.2.3-canary.890.11643.0`
- `@auto-canary/released@9.2.3-canary.890.11643.0`
- `@auto-canary/s3@9.2.3-canary.890.11643.0`
- `@auto-canary/slack@9.2.3-canary.890.11643.0`
- `@auto-canary/twitter@9.2.3-canary.890.11643.0`
- `@auto-canary/upload-assets@9.2.3-canary.890.11643.0`</details>
<!-- GITHUB_RELEASE PR BODY: canary-version -->



Patch:
diff --git a/plugins/first-time-contributor/src/index.ts b/plugins/first-time-contributor/src/index.ts
index 9ca922a2..c88a936c 100644
--- a/plugins/first-time-contributor/src/index.ts
+++ b/plugins/first-time-contributor/src/index.ts
@@ -1,25 +1,10 @@
-import { Auto, IPlugin, execPromise } from '@auto-it/core';
+import { Auto, IPlugin } from '@auto-it/core';
 import { ICommitAuthor } from '@auto-it/core/dist/log-parse';
 import flatMap from 'array.prototype.flatmap';
 import endent from 'endent';
 import urlJoin from 'url-join';
 import { URL } from 'url';
 
-const JUST_NAME = '%aN' as const;
-const JUST_EMAIL = '%cE' as const;
-
-type Format = typeof JUST_NAME | typeof JUST_EMAIL;
-
-/** Get some of the contributors from the git history */
-const getContributors = async (format: Format) => {
-  const start = '$(git rev-list HEAD | tail -n 1)';
-  const lastRelease = '$(git describe --tags --abbrev=0)';
-
-  return execPromise(
-    `git log --format='${format}' ${start}..${lastRelease} | sort -u`
-  );
-};
-
 /**
  * Thank first time contributors for their work right in your release notes.
  */
@@ -41,29 +26,27 @@ export default class FirstTimeContributorPlugin implements IPlugin {
       changelog.hooks.addToBody.tapPromise(
         this.name,
         async (notes, commits) => {
-          const authors = flatMap(commits, c => c.authors);
+          const newContributors = (
+            await Promise.all(
+              flatMap(commits, c => c.authors).map(async author => {
+                if (!author.username) {
+                  return;
+                }
 
-          const contributors = await Promise.all([
-            getContributors(JUST_NAME),
-            getContributors(JUST_EMAIL),
-            auto
-              .git!.github.repos.listContributors({
-                repo: auto.git!.options.repo,
-                owner: auto.git!.options.owner
-              })
-              .then(response =>
-                response.data.map(collaborator => collaborator.login).join('\n')
-              )
-          ]).then(lists => lists.join('\n').split('\n'));
+                const prs = await auto.git?.graphql(`
+                  {
+                    search(first: 2, type: ISSUE, query: "user:${auto.git?.options.owner} repo:${auto.git?.options.repo} author:${author.username} state:closed") {
+                      issueCount
+                    }
+                  }
+                `);
 
-          const newContributors = authors.filter(
-            ({ name = '', email = '', username = '' }) =>
-              !(
-                (name && contributors.includes(name)) ||
-                (email && contributors.includes(email)) ||
-                (username && contributors.includes(username))
-              )
-          );
+                if (prs && prs.search.issueCount <= 1) {
+                  return author;
+                }
+              })
+            )
+          ).filter((a): a is ICommitAuthor => Boolean(a));
 
           if (!newContributors.length) {
             return notes;
@@ -75,7 +58,7 @@ export default class FirstTimeContributorPlugin implements IPlugin {
           if (lines.size > 1) {
             thankYou = endent`
               :tada: This release contains work from new contributors! :tada:
-            
+
               Thanks for all your work!\n\n${[...lines]
                 .map(line => `:heart: ${line}`)
                 .join('\n\n')}
@@ -83,7 +66,7 @@ export default class FirstTimeContributorPlugin implements IPlugin {
           } else {
             thankYou = endent`
               :tada: This release contains work from a new contributor! :tada:
-            
+
               Thank you, ${[...lines][0]}, for all your work!
             `;
           }


Test patch:
diff --git a/plugins/first-time-contributor/__tests__/__snapshots__/first-time-contributor.test.ts.snap b/plugins/first-time-contributor/__tests__/__snapshots__/first-time-contributor.test.ts.snap
index 02f80f97..54c6fd56 100644
--- a/plugins/first-time-contributor/__tests__/__snapshots__/first-time-contributor.test.ts.snap
+++ b/plugins/first-time-contributor/__tests__/__snapshots__/first-time-contributor.test.ts.snap
@@ -4,7 +4,7 @@ exports[`First Time Contributor Plugin should exclude a past contributor 1`] = `
 Array [
   ":tada: This release contains work from a new contributor! :tada:
 
-Thank you, Jeff, for all your work!",
+Thank you, Jeff ([@Jeff](https://github.com/Jeff)), for all your work!",
 ]
 `;
 
@@ -12,7 +12,7 @@ exports[`First Time Contributor Plugin should exclude a past contributor by user
 Array [
   ":tada: This release contains work from a new contributor! :tada:
 
-Thank you, Jeff123, for all your work!",
+Thank you, Jeff123 ([@Jeff123](https://github.com/Jeff123)), for all your work!",
 ]
 `;
 
@@ -34,9 +34,9 @@ Array [
 
 Thanks for all your work!
 
-:heart: Jeff
+:heart: Jeff ([@Jeff](https://github.com/Jeff))
 
-:heart: Andrew",
+:heart: Andrew ([@Andrew](https://github.com/Andrew))",
 ]
 `;
 
@@ -48,6 +48,4 @@ Thank you, [@jeff-the-snake](https://github.com/jeff-the-snake), for all your wo
 ]
 `;
 
-exports[`First Time Contributor Plugin should not include past contributors - email 1`] = `Array []`;
-
 exports[`First Time Contributor Plugin should not include past contributors 1`] = `Array []`;
diff --git a/plugins/first-time-contributor/__tests__/first-time-contributor.test.ts b/plugins/first-time-contributor/__tests__/first-time-contributor.test.ts
index 3ef9c913..cc86c1e5 100644
--- a/plugins/first-time-contributor/__tests__/first-time-contributor.test.ts
+++ b/plugins/first-time-contributor/__tests__/first-time-contributor.test.ts
@@ -9,6 +9,7 @@ import {
 import FirstTimeContributor from '../src';
 import Octokit = require('@octokit/rest');
 
+const graphql = jest.fn();
 const exec = jest.fn();
 exec.mockReturnValue('');
 // @ts-ignore
@@ -24,6 +25,7 @@ const setup = (
   plugin.apply({
     hooks,
     git: {
+      graphql,
       options: { repo: 'repo', owner: 'test' },
       github: {
         repos: {
@@ -51,20 +53,30 @@ describe('First Time Contributor Plugin', () => {
   test('should include all authors with no past contributors', async () => {
     const hooks = setup();
     const commits = [
-      makeCommitFromMsg('foo', { name: 'Jeff' }),
-      makeCommitFromMsg('foo', { name: 'Andrew' })
+      makeCommitFromMsg('foo', { name: 'Jeff', username: 'Jeff' }),
+      makeCommitFromMsg('foo', { name: 'Andrew', username: 'Andrew' })
     ];
 
+    graphql.mockReturnValueOnce({
+      search: { issueCount: 0 }
+    });
+    graphql.mockReturnValueOnce({
+      search: { issueCount: 0 }
+    });
+
     expect(await hooks.addToBody.promise([], commits)).toMatchSnapshot();
   });
 
   test('should exclude a past contributor', async () => {
     const hooks = setup();
     const commits = [
-      makeCommitFromMsg('foo', { name: 'Jeff' }),
-      makeCommitFromMsg('foo', { name: 'Andrew' })
+      makeCommitFromMsg('foo', { name: 'Jeff', username: 'Jeff' }),
+      makeCommitFromMsg('foo', { name: 'Andrew', username: 'Andrew' })
     ];
-    exec.mockReturnValueOnce('Andrew');
+
+    graphql.mockReturnValueOnce({
+      search: { issueCount: 1 }
+    });
 
     expect(await hooks.addToBody.promise([], commits)).toMatchSnapshot();
   });
@@ -72,31 +84,27 @@ describe('First Time Contributor Plugin', () => {
   test('should exclude a past contributor by username', async () => {
     const hooks = setup([{ login: 'Andrew420' }]);
     const commits = [
-      makeCommitFromMsg('foo', { name: 'Jeff123' }),
-      makeCommitFromMsg('foo', { name: 'Andrew420' })
+      makeCommitFromMsg('foo', { name: 'Jeff123', username: 'Jeff123' }),
+      makeCommitFromMsg('foo', { name: 'Andrew420', username: 'Andrew420' })
     ];
 
+    graphql.mockReturnValueOnce({
+      search: { issueCount: 1 }
+    });
+
     expect(await hooks.addToBody.promise([], commits)).toMatchSnapshot();
   });
 
   test('should not include past contributors', async () => {
     const hooks = setup();
     const commits = [
-      makeCommitFromMsg('foo', { name: 'Jeff' }),
-      makeCommitFromMsg('foo', { name: 'Andrew' })
+      makeCommitFromMsg('foo', { name: 'Jeff', username: 'Jeff' }),
+      makeCommitFromMsg('foo', { name: 'Andrew', username: 'Andrew' })
     ];
-    exec.mockReturnValueOnce('Jeff');
-    exec.mockReturnValueOnce('Andrew');
 
-    expect(await hooks.addToBody.promise([], commits)).toMatchSnapshot();
-  });
-
-  test('should not include past contributors - email', async () => {
-    const hooks = setup();
-    const commits = [
-      makeCommitFromMsg('foo', { name: 'Jeff', email: 'Jeff@email.com' })
-    ];
-    exec.mockReturnValueOnce('Jeff@email.com');
+    graphql.mockReturnValue({
+      search: { issueCount: 2 }
+    });
 
     expect(await hooks.addToBody.promise([], commits)).toMatchSnapshot();
   });
@@ -108,6 +116,10 @@ describe('First Time Contributor Plugin', () => {
       makeCommitFromMsg('foo', { name: 'Andrew', username: 'hipstersmoothie' })
     ];
 
+    graphql.mockReturnValue({
+      search: { issueCount: 1 }
+    });
+
     expect(await hooks.addToBody.promise([], commits)).toMatchSnapshot();
   });
 
@@ -117,6 +129,10 @@ describe('First Time Contributor Plugin', () => {
       makeCommitFromMsg('foo', { username: 'jeff-the-snake', name: '' })
     ];
 
+    graphql.mockReturnValue({
+      search: { issueCount: 1 }
+    });
+
     expect(await hooks.addToBody.promise([], commits)).toMatchSnapshot();
   });
 });