diff --git a/package-lock.json b/package-lock.json index c65700f2300270..277652195f2763 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11584,6 +11584,15 @@ "@babel/runtime": "^7.8.3" } }, + "@wordpress/project-management-automation": { + "version": "file:packages/project-management-automation", + "dev": true, + "requires": { + "@actions/core": "^1.0.0", + "@actions/github": "^1.0.0", + "@babel/runtime": "^7.8.3" + } + }, "@wordpress/redux-routine": { "version": "file:packages/redux-routine", "requires": { @@ -29876,6 +29885,35 @@ "lower-case": "^1.1.1" } }, + "nock": { + "version": "12.0.3", + "resolved": "https://registry.npmjs.org/nock/-/nock-12.0.3.tgz", + "integrity": "sha512-QNb/j8kbFnKCiyqi9C5DD0jH/FubFGj5rt9NQFONXwQm3IPB0CULECg/eS3AU1KgZb/6SwUa4/DTRKhVxkGABw==", + "dev": true, + "requires": { + "debug": "^4.1.0", + "json-stringify-safe": "^5.0.1", + "lodash": "^4.17.13", + "propagate": "^2.0.0" + }, + "dependencies": { + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, "node-dir": { "version": "0.1.17", "resolved": "https://registry.npmjs.org/node-dir/-/node-dir-0.1.17.tgz", @@ -33063,6 +33101,12 @@ "reflect.ownkeys": "^0.2.0" } }, + "propagate": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/propagate/-/propagate-2.0.1.tgz", + "integrity": "sha512-vGrhOavPSTz4QVNuBNdcNXePNdNMaO1xj9yBeH1ScQPjk/rhg9sSlCXPhMkFuaNNW/syTvYqsnbIJxMBfRbbag==", + "dev": true + }, "property-information": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/property-information/-/property-information-5.3.0.tgz", diff --git a/package.json b/package.json index e0797d32f52d27..ebdbd2d6dc50a1 100644 --- a/package.json +++ b/package.json @@ -113,6 +113,7 @@ "@wordpress/npm-package-json-lint-config": "file:packages/npm-package-json-lint-config", "@wordpress/postcss-themes": "file:packages/postcss-themes", "@wordpress/prettier-config": "file:packages/prettier-config", + "@wordpress/project-management-automation": "file:packages/project-management-automation", "@wordpress/scripts": "file:packages/scripts", "babel-loader": "8.0.6", "babel-plugin-emotion": "10.0.27", @@ -151,6 +152,7 @@ "metro-react-native-babel-preset": "0.55.0", "metro-react-native-babel-transformer": "0.55.0", "mkdirp": "0.5.1", + "nock": "12.0.3", "node-sass": "4.12.0", "node-watch": "0.6.0", "postcss": "7.0.13", diff --git a/packages/project-management-automation/CHANGELOG.md b/packages/project-management-automation/CHANGELOG.md index c7363d0d385105..0e2f5ad00a4104 100644 --- a/packages/project-management-automation/CHANGELOG.md +++ b/packages/project-management-automation/CHANGELOG.md @@ -3,6 +3,11 @@ ### New feature - Include TypeScript type declarations ([#18942](https://github.com/WordPress/gutenberg/pull/18942)) +- The "Add First Time Contributor Label" task now prompts the user to link their GitHub account to their WordPress.org profile if neccessary for props credit. The task has been renamed "First Time Contributor". + +### Improvements + +- The "Add First Time Contributor Label" task now runs retroactively on pushes to master, due to [permission constraints](https://help.github.com/en/actions/configuring-and-managing-workflows/authenticating-with-the-github_token#permissions-for-the-github_token) of GitHub Actions. ## 1.0.0 (2019-08-29) diff --git a/packages/project-management-automation/README.md b/packages/project-management-automation/README.md index 40c85f70a67233..17726771d94e66 100644 --- a/packages/project-management-automation/README.md +++ b/packages/project-management-automation/README.md @@ -2,7 +2,7 @@ This is a [GitHub Action](https://help.github.com/en/categories/automating-your-workflow-with-github-actions) which contains various automation to assist with managing the Gutenberg GitHub repository: -- `add-first-time-contributor-label`: Adds the 'First Time Contributor' label to PRs opened by contributors that have not yet made a commit. +- `first-time-contributor`: Adds the 'First Time Contributor' label to PRs merged on behalf of contributors that have not previously made a contribution, and prompts the user to link their GitHub account to their WordPress.org profile if neccessary for props credit. - `add-milestone`: Assigns the correct milestone to PRs once merged. - `assign-fixed-issues`: Assigns any issues 'fixed' by a newly opened PR to the author of that PR. diff --git a/packages/project-management-automation/lib/add-first-time-contributor-label.js b/packages/project-management-automation/lib/add-first-time-contributor-label.js deleted file mode 100644 index 661a2412b03ac5..00000000000000 --- a/packages/project-management-automation/lib/add-first-time-contributor-label.js +++ /dev/null @@ -1,50 +0,0 @@ -/** - * Internal dependencies - */ -const debug = require( './debug' ); - -/** @typedef {import('@actions/github').GitHub} GitHub */ -/** @typedef {import('@octokit/webhooks').WebhookPayloadPullRequest} WebhookPayloadPullRequest */ - -/** - * Adds the 'First Time Contributor' label to PRs opened by contributors that - * have not yet made a commit. - * - * @param {WebhookPayloadPullRequest} payload Pull request event payload. - * @param {GitHub} octokit Initialized Octokit REST client. - */ -async function addFirstTimeContributorLabel( payload, octokit ) { - const owner = payload.repository.owner.login; - const repo = payload.repository.name; - const author = payload.pull_request.user.login; - - debug( - `add-first-time-contributor-label: Searching for commits in ${ owner }/${ repo } by @${ author }` - ); - - const { - data: { total_count: totalCount }, - } = await octokit.search.commits( { - q: `repo:${ owner }/${ repo }+author:${ author }`, - } ); - - if ( totalCount !== 0 ) { - debug( - `add-first-time-contributor-label: ${ totalCount } commits found. Aborting` - ); - return; - } - - debug( - `add-first-time-contributor-label: Adding 'First Time Contributor' label to issue #${ payload.pull_request.number }` - ); - - await octokit.issues.addLabels( { - owner, - repo, - issue_number: payload.pull_request.number, - labels: [ 'First-time Contributor' ], - } ); -} - -module.exports = addFirstTimeContributorLabel; diff --git a/packages/project-management-automation/lib/add-milestone.js b/packages/project-management-automation/lib/add-milestone.js index e1e8ae39bc99f4..1716a8bea4f024 100644 --- a/packages/project-management-automation/lib/add-milestone.js +++ b/packages/project-management-automation/lib/add-milestone.js @@ -2,6 +2,7 @@ * Internal dependencies */ const debug = require( './debug' ); +const getAssociatedPullRequest = require( './get-associated-pull-request' ); /** @typedef {import('@octokit/rest').HookError} HookError */ /** @typedef {import('@actions/github').GitHub} GitHub */ @@ -45,8 +46,7 @@ async function addMilestone( payload, octokit ) { return; } - const match = payload.commits[ 0 ].message.match( /\(#(\d+)\)$/m ); - const prNumber = match && match[ 1 ]; + const prNumber = getAssociatedPullRequest( payload.commits[ 0 ] ); if ( ! prNumber ) { debug( 'add-milestone: Commit is not a squashed PR. Aborting' ); return; diff --git a/packages/project-management-automation/lib/first-time-contributor.js b/packages/project-management-automation/lib/first-time-contributor.js new file mode 100644 index 00000000000000..a31093640c8909 --- /dev/null +++ b/packages/project-management-automation/lib/first-time-contributor.js @@ -0,0 +1,113 @@ +/** + * Internal dependencies + */ +const debug = require( './debug' ); +const getAssociatedPullRequest = require( './get-associated-pull-request' ); +const hasWordPressProfile = require( './has-wordpress-profile' ); + +/** @typedef {import('@actions/github').GitHub} GitHub */ +/** @typedef {import('@octokit/webhooks').WebhookPayloadPush} WebhookPayloadPush */ +/** @typedef {import('./get-associated-pull-request').WebhookPayloadPushCommit} WebhookPayloadPushCommit */ + +/** + * Message of comment prompting contributor to link their GitHub account from + * their WordPress.org profile for props credit. + * + * @type {string} + */ +const ACCOUNT_LINK_PROMPT = + "Congratulations on your first merged pull request! We'd like to credit " + + 'you for your contribution in the post announcing the next WordPress ' + + "release, but we can't find a WordPress.org profile associated with your " + + 'GitHub account. When you have a moment, visit the following URL and ' + + 'click "link your GitHub account" under "GitHub Username" to link your ' + + 'accounts:\n\nhttps://profiles.wordpress.org/me/profile/edit/\n\nAnd if ' + + "you don't have a WordPress.org account, you can create one on this page:" + + '\n\nhttps://login.wordpress.org/register\n\nKudos!'; + +/** + * Adds the 'First Time Contributor' label to PRs merged on behalf of + * contributors that have not yet made a commit, and prompts the user to link + * their GitHub account to their WordPress.org profile if neccessary for props + * credit. + * + * @param {WebhookPayloadPush} payload Push event payload. + * @param {GitHub} octokit Initialized Octokit REST client. + */ +async function firstTimeContributor( payload, octokit ) { + if ( payload.ref !== 'refs/heads/master' ) { + debug( 'first-time-contributor: Commit is not to `master`. Aborting' ); + return; + } + + const commit = + /** @type {WebhookPayloadPushCommit} */ ( payload.commits[ 0 ] ); + const pullRequest = getAssociatedPullRequest( commit ); + if ( ! pullRequest ) { + debug( + 'first-time-contributor: Cannot determine pull request associated with commit. Aborting' + ); + return; + } + + const repo = payload.repository.name; + const owner = payload.repository.owner.login; + const author = commit.author.username; + debug( + `first-time-contributor: Searching for commits in ${ owner }/${ repo } by @${ author }` + ); + + const { data: commits } = await octokit.repos.listCommits( { + owner, + repo, + author, + } ); + + if ( commits.length > 1 ) { + debug( + `first-time-contributor: Not the first commit for author. Aborting` + ); + return; + } + + debug( + `first-time-contributor: Adding 'First Time Contributor' label to issue #${ pullRequest }` + ); + + await octokit.issues.addLabels( { + owner, + repo, + issue_number: pullRequest, + labels: [ 'First-time Contributor' ], + } ); + + debug( + `first-time-contributor: Checking for WordPress username associated with @${ author }` + ); + + let hasProfile; + try { + hasProfile = await hasWordPressProfile( author ); + } catch ( error ) { + debug( + `first-time-contributor: Error retrieving from profile API:\n\n${ error.toString() }` + ); + return; + } + + if ( hasProfile ) { + debug( + `first-time-contributor: User already known. No need to prompt for account link!` + ); + return; + } + + await octokit.issues.createComment( { + owner, + repo, + issue_number: pullRequest, + body: ACCOUNT_LINK_PROMPT, + } ); +} + +module.exports = firstTimeContributor; diff --git a/packages/project-management-automation/lib/get-associated-pull-request.js b/packages/project-management-automation/lib/get-associated-pull-request.js new file mode 100644 index 00000000000000..6ec267a26a5b8e --- /dev/null +++ b/packages/project-management-automation/lib/get-associated-pull-request.js @@ -0,0 +1,39 @@ +/** + * @typedef WebhookPayloadPushCommitAuthor + * + * @property {string} name Author name. + * @property {string} email Author email. + * @property {string} username Author username. + */ + +/** + * Minimal type detail of GitHub Push webhook event payload, for lack of their + * own. + * + * TODO: If GitHub improves this on their own webhook payload types, this type + * should no longer be necessary. + * + * @typedef {Record} WebhookPayloadPushCommit + * + * @property {string} message Commit message. + * @property {WebhookPayloadPushCommitAuthor} author Commit author. + * + * @see https://developer.github.com/v3/activity/events/types/#pushevent + */ + +/** + * Given a commit object, returns a promise resolving with the pull request + * number associated with the commit, or null if an associated pull request + * cannot be determined. + * + * @param {WebhookPayloadPushCommit} commit Commit object. + * + * @return {number?} Pull request number, or null if it cannot be + * determined. + */ +function getAssociatedPullRequest( commit ) { + const match = commit.message.match( /\(#(\d+)\)$/m ); + return match && Number( match[ 1 ] ); +} + +module.exports = getAssociatedPullRequest; diff --git a/packages/project-management-automation/lib/has-wordpress-profile.js b/packages/project-management-automation/lib/has-wordpress-profile.js new file mode 100644 index 00000000000000..cb2adfdba71e0b --- /dev/null +++ b/packages/project-management-automation/lib/has-wordpress-profile.js @@ -0,0 +1,46 @@ +/** + * External dependencies + */ +const { request } = require( 'https' ); + +/** + * Endpoint hostname for WordPress.org profile lookup by GitHub username. + * + * @type {string} + */ +const BASE_PROFILE_LOOKUP_API_HOSTNAME = 'profiles.wordpress.org'; + +/** + * Base path for WordPress.org profile lookup by GitHub username. + * + * @type {string} + */ +const BASE_PROFILE_LOOKUP_API_BASE_PATH = '/wp-json/wporg-github/v1/lookup/'; + +/** + * Returns a promise resolving to a boolean indicating if the given GitHub + * username can be associated with a WordPress.org profile. + * + * @param {string} githubUsername GitHub username. + * + * @return {Promise} Promise resolving to whether WordPress profile is + * known. + */ +async function hasWordPressProfile( githubUsername ) { + return new Promise( ( resolve, reject ) => { + const options = { + hostname: BASE_PROFILE_LOOKUP_API_HOSTNAME, + path: BASE_PROFILE_LOOKUP_API_BASE_PATH + githubUsername, + method: 'HEAD', + headers: { + 'User-Agent': 'Gutenberg/project-management-automation', + }, + }; + + request( options, ( res ) => resolve( res.statusCode === 200 ) ) + .on( 'error', ( error ) => reject( error ) ) + .end(); + } ); +} + +module.exports = hasWordPressProfile; diff --git a/packages/project-management-automation/lib/index.js b/packages/project-management-automation/lib/index.js index 630bda89440e93..2b280d1d5d5248 100644 --- a/packages/project-management-automation/lib/index.js +++ b/packages/project-management-automation/lib/index.js @@ -8,7 +8,7 @@ const { context, GitHub } = require( '@actions/github' ); * Internal dependencies */ const assignFixedIssues = require( './assign-fixed-issues' ); -const addFirstTimeContributorLabel = require( './add-first-time-contributor-label' ); +const firstTimeContributor = require( './first-time-contributor' ); const addMilestone = require( './add-milestone' ); const debug = require( './debug' ); const ifNotFork = require( './if-not-fork' ); @@ -42,9 +42,8 @@ const automations = [ task: ifNotFork( assignFixedIssues ), }, { - event: 'pull_request', - action: 'opened', - task: ifNotFork( addFirstTimeContributorLabel ), + event: 'push', + task: firstTimeContributor, }, { event: 'push', diff --git a/packages/project-management-automation/lib/test/add-first-time-contributor-label.js b/packages/project-management-automation/lib/test/add-first-time-contributor-label.js deleted file mode 100644 index ac8e5d59f26a0c..00000000000000 --- a/packages/project-management-automation/lib/test/add-first-time-contributor-label.js +++ /dev/null @@ -1,74 +0,0 @@ -/** - * Internal dependencies - */ -import addFirstTimeContributorLabel from '../add-first-time-contributor-label'; - -describe( 'addFirstTimeContributorLabel', () => { - const payload = { - pull_request: { - user: { - login: 'matt', - }, - number: 123, - }, - repository: { - owner: { - login: 'WordPress', - }, - name: 'gutenberg', - }, - }; - - it( 'does nothing if the user has commits', async () => { - const octokit = { - search: { - commits: jest.fn( () => - Promise.resolve( { - data: { - total_count: 100, - }, - } ) - ), - }, - issues: { - addLabels: jest.fn(), - }, - }; - - await addFirstTimeContributorLabel( payload, octokit ); - - expect( octokit.search.commits ).toHaveBeenCalledWith( { - q: 'repo:WordPress/gutenberg+author:matt', - } ); - expect( octokit.issues.addLabels ).not.toHaveBeenCalled(); - } ); - - it( 'adds the label if the user does not have commits', async () => { - const octokit = { - search: { - commits: jest.fn( () => - Promise.resolve( { - data: { - total_count: 0, - }, - } ) - ), - }, - issues: { - addLabels: jest.fn(), - }, - }; - - await addFirstTimeContributorLabel( payload, octokit ); - - expect( octokit.search.commits ).toHaveBeenCalledWith( { - q: 'repo:WordPress/gutenberg+author:matt', - } ); - expect( octokit.issues.addLabels ).toHaveBeenCalledWith( { - owner: 'WordPress', - repo: 'gutenberg', - issue_number: 123, - labels: [ 'First-time Contributor' ], - } ); - } ); -} ); diff --git a/packages/project-management-automation/lib/test/add-milestone.js b/packages/project-management-automation/lib/test/add-milestone.js index 63f49811f9692e..13ecd80aa9b0a2 100644 --- a/packages/project-management-automation/lib/test/add-milestone.js +++ b/packages/project-management-automation/lib/test/add-milestone.js @@ -63,7 +63,7 @@ describe( 'addMilestone', () => { expect( octokit.issues.get ).toHaveBeenCalledWith( { owner: 'WordPress', repo: 'gutenberg', - issue_number: '123', + issue_number: 123, } ); expect( octokit.issues.createMilestone ).not.toHaveBeenCalled(); expect( octokit.issues.listMilestonesForRepo ).not.toHaveBeenCalled(); @@ -124,7 +124,7 @@ describe( 'addMilestone', () => { expect( octokit.issues.get ).toHaveBeenCalledWith( { owner: 'WordPress', repo: 'gutenberg', - issue_number: '123', + issue_number: 123, } ); expect( octokit.repos.getContents ).toHaveBeenCalledWith( { owner: 'WordPress', @@ -144,7 +144,7 @@ describe( 'addMilestone', () => { expect( octokit.issues.update ).toHaveBeenCalledWith( { owner: 'WordPress', repo: 'gutenberg', - issue_number: '123', + issue_number: 123, milestone: 12, } ); } ); @@ -202,7 +202,7 @@ describe( 'addMilestone', () => { expect( octokit.issues.get ).toHaveBeenCalledWith( { owner: 'WordPress', repo: 'gutenberg', - issue_number: '123', + issue_number: 123, } ); expect( octokit.repos.getContents ).toHaveBeenCalledWith( { owner: 'WordPress', @@ -222,7 +222,7 @@ describe( 'addMilestone', () => { expect( octokit.issues.update ).toHaveBeenCalledWith( { owner: 'WordPress', repo: 'gutenberg', - issue_number: '123', + issue_number: 123, milestone: 12, } ); } ); diff --git a/packages/project-management-automation/lib/test/first-time-contributor.js b/packages/project-management-automation/lib/test/first-time-contributor.js new file mode 100644 index 00000000000000..27a328c2b5948e --- /dev/null +++ b/packages/project-management-automation/lib/test/first-time-contributor.js @@ -0,0 +1,216 @@ +/** + * Internal dependencies + */ +import firstTimeContributor from '../first-time-contributor'; +import hasWordPressProfile from '../has-wordpress-profile'; + +jest.mock( '../has-wordpress-profile', () => jest.fn() ); + +describe( 'firstTimeContributor', () => { + beforeEach( () => { + hasWordPressProfile.mockReset(); + } ); + + const payload = { + ref: 'refs/heads/master', + commits: [ + { + id: '4c535288a6a2b75ff23ee96c75f7d9877e919241', + message: 'Add a feature from pull request (#123)', + author: { + name: 'Ghost', + email: 'ghost@example.invalid', + username: 'ghost', + }, + }, + ], + repository: { + owner: { + login: 'WordPress', + }, + name: 'gutenberg', + }, + }; + + it( 'does nothing if not a commit to master', async () => { + const payloadForBranchPush = { + ...payload, + ref: 'refs/heads/update/chicken-branch', + }; + + const octokit = { + repos: { + listCommits: jest.fn(), + }, + }; + + await firstTimeContributor( payloadForBranchPush, octokit ); + + expect( octokit.repos.listCommits ).not.toHaveBeenCalled(); + } ); + + it( 'does nothing if commit pull request undeterminable', async () => { + const payloadDirectToMaster = { + ...payload, + commits: [ + { + message: 'Add a feature direct to master', + author: { + name: 'Ghost', + email: 'ghost@example.invalid', + username: 'ghost', + }, + }, + ], + }; + + const octokit = { + repos: { + listCommits: jest.fn(), + }, + }; + + await firstTimeContributor( payloadDirectToMaster, octokit ); + + expect( octokit.repos.listCommits ).not.toHaveBeenCalled(); + } ); + + it( 'does nothing if the user has multiple commits', async () => { + const octokit = { + repos: { + listCommits: jest.fn( () => + Promise.resolve( { + data: [ + { sha: '4c535288a6a2b75ff23ee96c75f7d9877e919241' }, + { sha: '59b07cc57adff90630fc9d5cf2317269a0f4f158' }, + ], + } ) + ), + }, + issues: { + addLabels: jest.fn(), + }, + }; + + await firstTimeContributor( payload, octokit ); + + expect( octokit.repos.listCommits ).toHaveBeenCalledWith( { + owner: 'WordPress', + repo: 'gutenberg', + author: 'ghost', + } ); + expect( octokit.issues.addLabels ).not.toHaveBeenCalled(); + } ); + + it( 'adds the label if this was the first commit for the user', async () => { + const octokit = { + repos: { + listCommits: jest.fn( () => + Promise.resolve( { + data: [ + { sha: '4c535288a6a2b75ff23ee96c75f7d9877e919241' }, + ], + } ) + ), + }, + issues: { + addLabels: jest.fn(), + createComment: jest.fn(), + }, + }; + + hasWordPressProfile.mockReturnValue( Promise.resolve( true ) ); + + await firstTimeContributor( payload, octokit ); + + expect( octokit.repos.listCommits ).toHaveBeenCalledWith( { + owner: 'WordPress', + repo: 'gutenberg', + author: 'ghost', + } ); + expect( octokit.issues.addLabels ).toHaveBeenCalledWith( { + owner: 'WordPress', + repo: 'gutenberg', + issue_number: 123, + labels: [ 'First-time Contributor' ], + } ); + expect( octokit.issues.createComment ).not.toHaveBeenCalled(); + } ); + + it( 'aborts if the request to retrieve WordPress.org user profile fails', async () => { + const octokit = { + repos: { + listCommits: jest.fn( () => + Promise.resolve( { + data: [ + { sha: '4c535288a6a2b75ff23ee96c75f7d9877e919241' }, + ], + } ) + ), + }, + issues: { + addLabels: jest.fn(), + createComment: jest.fn(), + }, + }; + + hasWordPressProfile.mockImplementation( () => { + return Promise.reject( new Error( 'Whoops!' ) ); + } ); + + await firstTimeContributor( payload, octokit ); + + expect( octokit.repos.listCommits ).toHaveBeenCalledWith( { + owner: 'WordPress', + repo: 'gutenberg', + author: 'ghost', + } ); + expect( octokit.issues.addLabels ).toHaveBeenCalledWith( { + owner: 'WordPress', + repo: 'gutenberg', + issue_number: 123, + labels: [ 'First-time Contributor' ], + } ); + expect( octokit.issues.createComment ).not.toHaveBeenCalled(); + } ); + + it( 'prompts the user to link their GitHub account to their WordPress.org profile', async () => { + const octokit = { + repos: { + listCommits: jest.fn( () => + Promise.resolve( { + data: [ + { sha: '4c535288a6a2b75ff23ee96c75f7d9877e919241' }, + ], + } ) + ), + }, + issues: { + addLabels: jest.fn(), + createComment: jest.fn(), + }, + }; + + hasWordPressProfile.mockReturnValue( Promise.resolve( false ) ); + + await firstTimeContributor( payload, octokit ); + + expect( octokit.repos.listCommits ).toHaveBeenCalledWith( { + owner: 'WordPress', + repo: 'gutenberg', + author: 'ghost', + } ); + expect( octokit.issues.addLabels ).toHaveBeenCalledWith( { + owner: 'WordPress', + repo: 'gutenberg', + issue_number: 123, + labels: [ 'First-time Contributor' ], + } ); + expect( octokit.issues.createComment ).toHaveBeenCalledWith( { + owner: 'WordPress', + repo: 'gutenberg', + issue_number: 123, + body: expect.stringMatching( /^Congratulations/ ), + } ); + } ); +} ); diff --git a/packages/project-management-automation/lib/test/get-associated-pull-request.js b/packages/project-management-automation/lib/test/get-associated-pull-request.js new file mode 100644 index 00000000000000..1e00d636e94e2a --- /dev/null +++ b/packages/project-management-automation/lib/test/get-associated-pull-request.js @@ -0,0 +1,37 @@ +/** + * Internal dependencies + */ +import getAssociatedPullRequest from '../get-associated-pull-request'; + +/** @typedef {import('../get-associated-pull-request').WebhookPayloadPushCommit} WebhookPayloadPushCommit */ + +/** + * An example commit which can be associated with a pull request, e.g. a pull + * request merge commit. + * + * @type {WebhookPayloadPushCommit} + */ +const VALID_COMMIT = { + message: + 'Components: SlotFill: Guard property access to possibly-undefined slot (#21205)', +}; + +/** + * An example commit which cannot be associated with a pull request, e.g. when + * someone commits directly to master. + * + * @type {WebhookPayloadPushCommit} + */ +const INVALID_COMMIT = { + message: 'Add basic placeholder content to post title, content, and date.', +}; + +describe( 'getAssociatedPullRequest', () => { + it( 'should return the pull request number associated with a commit', () => { + expect( getAssociatedPullRequest( VALID_COMMIT ) ).toBe( 21205 ); + } ); + + it( 'should return null if a pull request cannot be determined', () => { + expect( getAssociatedPullRequest( INVALID_COMMIT ) ).toBeNull(); + } ); +} ); diff --git a/packages/project-management-automation/lib/test/has-wordpress-profile.js b/packages/project-management-automation/lib/test/has-wordpress-profile.js new file mode 100644 index 00000000000000..9cb13544a245a1 --- /dev/null +++ b/packages/project-management-automation/lib/test/has-wordpress-profile.js @@ -0,0 +1,31 @@ +/** + * External dependencies + */ +import nock from 'nock'; + +/** + * Internal dependencies + */ +import hasWordPressProfile from '../has-wordpress-profile'; + +describe( 'hasWordPressProfile', () => { + it( 'resolves as false for missing profile', async () => { + nock( 'https://profiles.wordpress.org' ) + .intercept( '/wp-json/wporg-github/v1/lookup/ghost', 'HEAD' ) + .reply( 404 ); + + const result = await hasWordPressProfile( 'ghost' ); + + expect( result ).toBe( false ); + } ); + + it( 'resolves as true for known profile', async () => { + nock( 'https://profiles.wordpress.org' ) + .intercept( '/wp-json/wporg-github/v1/lookup/m', 'HEAD' ) + .reply( 200 ); + + const result = await hasWordPressProfile( 'm' ); + + expect( result ).toBe( true ); + } ); +} );