Skip to content

Commit

Permalink
Project Management: Prompt user to link GitHub account to WordPress.o…
Browse files Browse the repository at this point in the history
…rg profile (#21221)

* Project Management Automation: Add getAssociatedPullRequest utility

* Project Management Automation: Add First-Time Contributor label on push

* Project Management Automation: Include as dependency in root package

* Project Management Automation: Prompt user to link GitHub account to profile

* Project Management Automation: Update documentation for first contribution prompt

* Project Management Automation: Check for single commit of author

* Project Management Automation: Fetch profile by https HEAD

* Project Management Automation: Fix hostname to use hostname

* Project Management Automation: Add hasWordPressProfile tests

* Project Management Automation: Add request User-Agent header

* Project Management Automation: Update contributor prompt text

* Project Management Automation: Rename addFirstTimeContributorLabel to firstTimeContributor

* Project Management Automation: Expand function comment to include new behavior

* Project Management Automation: Expand CHANGELOG to include rename
  • Loading branch information
aduth authored Apr 1, 2020
1 parent 307e1fb commit 4f144b4
Show file tree
Hide file tree
Showing 15 changed files with 544 additions and 136 deletions.
44 changes: 44 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
5 changes: 5 additions & 0 deletions packages/project-management-automation/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
2 changes: 1 addition & 1 deletion packages/project-management-automation/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down

This file was deleted.

4 changes: 2 additions & 2 deletions packages/project-management-automation/lib/add-milestone.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand Down Expand Up @@ -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;
Expand Down
113 changes: 113 additions & 0 deletions packages/project-management-automation/lib/first-time-contributor.js
Original file line number Diff line number Diff line change
@@ -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;
Original file line number Diff line number Diff line change
@@ -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<string,*>} 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;
Original file line number Diff line number Diff line change
@@ -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<boolean>} 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;
7 changes: 3 additions & 4 deletions packages/project-management-automation/lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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' );
Expand Down Expand Up @@ -42,9 +42,8 @@ const automations = [
task: ifNotFork( assignFixedIssues ),
},
{
event: 'pull_request',
action: 'opened',
task: ifNotFork( addFirstTimeContributorLabel ),
event: 'push',
task: firstTimeContributor,
},
{
event: 'push',
Expand Down
Loading

0 comments on commit 4f144b4

Please sign in to comment.