| 
									
										
										
										
											2020-03-03 10:16:18 -08:00
										 |  |  | /** | 
					
						
							|  |  |  |  * @license | 
					
						
							| 
									
										
										
										
											2020-05-19 12:08:49 -07:00
										 |  |  |  * Copyright Google LLC All Rights Reserved. | 
					
						
							| 
									
										
										
										
											2020-03-03 10:16:18 -08:00
										 |  |  |  * | 
					
						
							|  |  |  |  * Use of this source code is governed by an MIT-style license that can be | 
					
						
							|  |  |  |  * found in the LICENSE file at https://angular.io/license
 | 
					
						
							|  |  |  |  */ | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | /** | 
					
						
							|  |  |  |  * This script gets contribution stats for all members of the angular org, | 
					
						
							|  |  |  |  * since a provided date. | 
					
						
							|  |  |  |  * The script expects the following flag(s): | 
					
						
							|  |  |  |  * | 
					
						
							|  |  |  |  * required: | 
					
						
							|  |  |  |  *   --since [date] The data after which contributions are queried for. | 
					
						
							|  |  |  |  *       Uses githubs search format for dates, e.g. "2020-01-21". | 
					
						
							|  |  |  |  *       See | 
					
						
							|  |  |  |  * https://help.github.com/en/github/searching-for-information-on-github/understanding-the-search-syntax#query-for-dates
 | 
					
						
							|  |  |  |  * | 
					
						
							|  |  |  |  * optional: | 
					
						
							|  |  |  |  *  --use-created [boolean] If the created timestamp should be used for | 
					
						
							|  |  |  |  *     time comparisons, defaults otherwise to the updated timestamp. | 
					
						
							|  |  |  |  */ | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | import {graphql as unauthenticatedGraphql} from '@octokit/graphql'; | 
					
						
							|  |  |  | import * as minimist from 'minimist'; | 
					
						
							|  |  |  | import {alias, params, query as graphqlQuery, types} from 'typed-graphqlify'; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // The organization to be considered for the queries.
 | 
					
						
							|  |  |  | const ORG = 'angular'; | 
					
						
							|  |  |  | // The repositories to be considered for the queries.
 | 
					
						
							|  |  |  | const REPOS = ['angular', 'components', 'angular-cli']; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | /** | 
					
						
							|  |  |  |  * Handle flags for the script. | 
					
						
							|  |  |  |  */ | 
					
						
							|  |  |  | const args = minimist(process.argv.slice(2), { | 
					
						
							|  |  |  |   string: ['since'], | 
					
						
							|  |  |  |   boolean: ['use-created'], | 
					
						
							|  |  |  |   unknown: (option: string) => { | 
					
						
							|  |  |  |     console.error(`Unknown option: ${option}`); | 
					
						
							|  |  |  |     process.exit(1); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | if (!args['since']) { | 
					
						
							|  |  |  |   console.error(`Please provide --since [date]`); | 
					
						
							|  |  |  |   process.exit(1); | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | /** | 
					
						
							|  |  |  |  * Authenticated instance of Github GraphQl API service, relies on a | 
					
						
							|  |  |  |  * personal access token being available in the TOKEN environment variable. | 
					
						
							|  |  |  |  */ | 
					
						
							|  |  |  | const graphql = unauthenticatedGraphql.defaults({ | 
					
						
							|  |  |  |   headers: { | 
					
						
							|  |  |  |     // TODO(josephperrott): Remove reference to TOKEN environment variable as part of larger
 | 
					
						
							|  |  |  |     // effort to migrate to expecting tokens via GITHUB_ACCESS_TOKEN environment variables.
 | 
					
						
							|  |  |  |     authorization: `token ${process.env.TOKEN || process.env.GITHUB_ACCESS_TOKEN}`, | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | /** | 
					
						
							|  |  |  |  * Retrieves all current members of an organization. | 
					
						
							|  |  |  |  */ | 
					
						
							|  |  |  | async function getAllOrgMembers() { | 
					
						
							|  |  |  |   // The GraphQL query object to get a page of members of an organization.
 | 
					
						
							|  |  |  |   const MEMBERS_QUERY = params( | 
					
						
							|  |  |  |       { | 
					
						
							|  |  |  |         $first: 'Int',      // How many entries to get with each request
 | 
					
						
							|  |  |  |         $after: 'String',   // The cursor to start the page at
 | 
					
						
							|  |  |  |         $owner: 'String!',  // The organization to query for
 | 
					
						
							|  |  |  |       }, | 
					
						
							|  |  |  |       { | 
					
						
							|  |  |  |         organization: params({login: '$owner'}, { | 
					
						
							|  |  |  |           membersWithRole: params( | 
					
						
							|  |  |  |               { | 
					
						
							|  |  |  |                 first: '$first', | 
					
						
							|  |  |  |                 after: '$after', | 
					
						
							|  |  |  |               }, | 
					
						
							|  |  |  |               { | 
					
						
							|  |  |  |                 nodes: [{login: types.string}], | 
					
						
							|  |  |  |                 pageInfo: { | 
					
						
							|  |  |  |                   hasNextPage: types.boolean, | 
					
						
							|  |  |  |                   endCursor: types.string, | 
					
						
							|  |  |  |                 }, | 
					
						
							|  |  |  |               }), | 
					
						
							|  |  |  |         }) | 
					
						
							|  |  |  |       }); | 
					
						
							|  |  |  |   const query = graphqlQuery('members', MEMBERS_QUERY); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   /** | 
					
						
							|  |  |  |    * Gets the query and queryParams for a specific page of entries. | 
					
						
							|  |  |  |    */ | 
					
						
							|  |  |  |   const queryBuilder = (count: number, cursor?: string) => { | 
					
						
							|  |  |  |     return { | 
					
						
							|  |  |  |       query, | 
					
						
							|  |  |  |       params: { | 
					
						
							|  |  |  |         after: cursor || null, | 
					
						
							|  |  |  |         first: count, | 
					
						
							|  |  |  |         owner: ORG, | 
					
						
							|  |  |  |       }, | 
					
						
							|  |  |  |     }; | 
					
						
							|  |  |  |   }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   // The current cursor
 | 
					
						
							|  |  |  |   let cursor = undefined; | 
					
						
							|  |  |  |   // If an additional page of members is expected
 | 
					
						
							|  |  |  |   let hasNextPage = true; | 
					
						
							|  |  |  |   // Array of Github usernames of the organization
 | 
					
						
							|  |  |  |   const members: string[] = []; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   while (hasNextPage) { | 
					
						
							|  |  |  |     const {query, params} = queryBuilder(100, cursor); | 
					
						
							|  |  |  |     const results = await graphql(query, params) as typeof MEMBERS_QUERY; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     results.organization.membersWithRole.nodes.forEach( | 
					
						
							|  |  |  |         (node: {login: string}) => members.push(node.login)); | 
					
						
							|  |  |  |     hasNextPage = results.organization.membersWithRole.pageInfo.hasNextPage; | 
					
						
							|  |  |  |     cursor = results.organization.membersWithRole.pageInfo.endCursor; | 
					
						
							|  |  |  |   } | 
					
						
							| 
									
										
										
										
											2020-03-10 22:24:28 +02:00
										 |  |  |   return members.sort(); | 
					
						
							| 
									
										
										
										
											2020-03-03 10:16:18 -08:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | /** | 
					
						
							|  |  |  |  * Build metadata for making requests for a specific user and date. | 
					
						
							|  |  |  |  * | 
					
						
							|  |  |  |  * Builds GraphQL query string, Query Params and Labels for making queries to GraphQl. | 
					
						
							|  |  |  |  */ | 
					
						
							|  |  |  | function buildQueryAndParams(username: string, date: string) { | 
					
						
							|  |  |  |   // Whether the updated or created timestamp should be used.
 | 
					
						
							|  |  |  |   const updatedOrCreated = args['use-created'] ? 'created' : 'updated'; | 
					
						
							|  |  |  |   let dataQueries: {[key: string]: {query: string, label: string}} = {}; | 
					
						
							|  |  |  |   // Add queries and params for all values queried for each repo.
 | 
					
						
							|  |  |  |   for (let repo of REPOS) { | 
					
						
							|  |  |  |     dataQueries = { | 
					
						
							|  |  |  |       ...dataQueries, | 
					
						
							|  |  |  |       [`${repo.replace(/[\/\-]/g, '_')}_issue_author`]: { | 
					
						
							|  |  |  |         query: `repo:${ORG}/${repo} is:issue author:${username} ${updatedOrCreated}:>${date}`, | 
					
						
							|  |  |  |         label: `${ORG}/${repo} Issue Authored`, | 
					
						
							|  |  |  |       }, | 
					
						
							|  |  |  |       [`${repo.replace(/[\/\-]/g, '_')}_issues_involved`]: { | 
					
						
							| 
									
										
										
										
											2020-04-13 16:40:21 -07:00
										 |  |  |         query: `repo:${ORG}/${repo} is:issue -author:${username} involves:${username} ${ | 
					
						
							|  |  |  |             updatedOrCreated}:>${date}`,
 | 
					
						
							| 
									
										
										
										
											2020-03-03 10:16:18 -08:00
										 |  |  |         label: `${ORG}/${repo} Issue Involved`, | 
					
						
							|  |  |  |       }, | 
					
						
							|  |  |  |       [`${repo.replace(/[\/\-]/g, '_')}_pr_author`]: { | 
					
						
							|  |  |  |         query: `repo:${ORG}/${repo} is:pr author:${username} ${updatedOrCreated}:>${date}`, | 
					
						
							|  |  |  |         label: `${ORG}/${repo} PR Author`, | 
					
						
							|  |  |  |       }, | 
					
						
							|  |  |  |       [`${repo.replace(/[\/\-]/g, '_')}_pr_involved`]: { | 
					
						
							|  |  |  |         query: `repo:${ORG}/${repo} is:pr involves:${username} ${updatedOrCreated}:>${date}`, | 
					
						
							|  |  |  |         label: `${ORG}/${repo} PR Involved`, | 
					
						
							|  |  |  |       }, | 
					
						
							|  |  |  |       [`${repo.replace(/[\/\-]/g, '_')}_pr_reviewed`]: { | 
					
						
							| 
									
										
										
										
											2020-04-13 16:40:21 -07:00
										 |  |  |         query: `repo:${ORG}/${repo} is:pr -author:${username} reviewed-by:${username} ${ | 
					
						
							|  |  |  |             updatedOrCreated}:>${date}`,
 | 
					
						
							| 
									
										
										
										
											2020-03-03 10:16:18 -08:00
										 |  |  |         label: `${ORG}/${repo} PR Reviewed`, | 
					
						
							|  |  |  |       }, | 
					
						
							|  |  |  |       [`${repo.replace(/[\/\-]/g, '_')}_pr_commented`]: { | 
					
						
							| 
									
										
										
										
											2020-04-13 16:40:21 -07:00
										 |  |  |         query: `repo:${ORG}/${repo} is:pr -author:${username} commenter:${username} ${ | 
					
						
							|  |  |  |             updatedOrCreated}:>${date}`,
 | 
					
						
							| 
									
										
										
										
											2020-03-03 10:16:18 -08:00
										 |  |  |         label: `${ORG}/${repo} PR Commented`, | 
					
						
							|  |  |  |       }, | 
					
						
							|  |  |  |     }; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  |   // Add queries and params for all values queried for the org.
 | 
					
						
							|  |  |  |   dataQueries = { | 
					
						
							|  |  |  |     ...dataQueries, | 
					
						
							|  |  |  |     [`${ORG}_org_issue_author`]: { | 
					
						
							|  |  |  |       query: `org:${ORG} is:issue author:${username} ${updatedOrCreated}:>${date}`, | 
					
						
							|  |  |  |       label: `${ORG} org Issue Authored`, | 
					
						
							|  |  |  |     }, | 
					
						
							|  |  |  |     [`${ORG}_org_issues_involved`]: { | 
					
						
							| 
									
										
										
										
											2020-04-13 16:40:21 -07:00
										 |  |  |       query: `org:${ORG} is:issue -author:${username} involves:${username} ${updatedOrCreated}:>${ | 
					
						
							|  |  |  |           date}`,
 | 
					
						
							| 
									
										
										
										
											2020-03-03 10:16:18 -08:00
										 |  |  |       label: `${ORG} org Issue Involved`, | 
					
						
							|  |  |  |     }, | 
					
						
							|  |  |  |     [`${ORG}_org_pr_author`]: { | 
					
						
							|  |  |  |       query: `org:${ORG} is:pr author:${username} ${updatedOrCreated}:>${date}`, | 
					
						
							|  |  |  |       label: `${ORG} org PR Author`, | 
					
						
							|  |  |  |     }, | 
					
						
							|  |  |  |     [`${ORG}_org_pr_involved`]: { | 
					
						
							|  |  |  |       query: `org:${ORG} is:pr involves:${username} ${updatedOrCreated}:>${date}`, | 
					
						
							|  |  |  |       label: `${ORG} org PR Involved`, | 
					
						
							|  |  |  |     }, | 
					
						
							|  |  |  |     [`${ORG}_org_pr_reviewed`]: { | 
					
						
							| 
									
										
										
										
											2020-04-13 16:40:21 -07:00
										 |  |  |       query: `org:${ORG} is:pr -author:${username} reviewed-by:${username} ${updatedOrCreated}:>${ | 
					
						
							|  |  |  |           date}`,
 | 
					
						
							| 
									
										
										
										
											2020-03-03 10:16:18 -08:00
										 |  |  |       label: `${ORG} org PR Reviewed`, | 
					
						
							|  |  |  |     }, | 
					
						
							|  |  |  |     [`${ORG}_org_pr_commented`]: { | 
					
						
							|  |  |  |       query: | 
					
						
							|  |  |  |           `org:${ORG} is:pr -author:${username} commenter:${username} ${updatedOrCreated}:>${date}`, | 
					
						
							|  |  |  |       label: `${ORG} org PR Commented`, | 
					
						
							|  |  |  |     }, | 
					
						
							|  |  |  |   }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   /** | 
					
						
							|  |  |  |    * Gets the labels for each requested value to be used as headers. | 
					
						
							|  |  |  |    */ | 
					
						
							|  |  |  |   function getLabels(pairs: typeof dataQueries) { | 
					
						
							|  |  |  |     return Object.values(pairs).map(val => val.label); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   /** | 
					
						
							|  |  |  |    * Gets the graphql query object for the GraphQL query. | 
					
						
							|  |  |  |    */ | 
					
						
							|  |  |  |   function getQuery(pairs: typeof dataQueries) { | 
					
						
							|  |  |  |     const output: {[key: string]: {}} = {}; | 
					
						
							|  |  |  |     Object.entries(pairs).map(([key, val]) => { | 
					
						
							|  |  |  |       output[alias(key, 'search')] = params( | 
					
						
							|  |  |  |           { | 
					
						
							|  |  |  |             query: `"${val.query}"`, | 
					
						
							|  |  |  |             type: 'ISSUE', | 
					
						
							|  |  |  |           }, | 
					
						
							|  |  |  |           { | 
					
						
							|  |  |  |             issueCount: types.number, | 
					
						
							|  |  |  |           }); | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  |     return output; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   return { | 
					
						
							|  |  |  |     query: graphqlQuery(getQuery(dataQueries)), | 
					
						
							|  |  |  |     labels: getLabels(dataQueries), | 
					
						
							|  |  |  |   }; | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | /** | 
					
						
							|  |  |  |  * Runs the script to create a CSV string with the requested data for each member | 
					
						
							|  |  |  |  * of the organization. | 
					
						
							|  |  |  |  */ | 
					
						
							|  |  |  | async function run(date: string) { | 
					
						
							| 
									
										
										
										
											2020-03-11 09:01:44 +01:00
										 |  |  |   try { | 
					
						
							|  |  |  |     const allOrgMembers = await getAllOrgMembers(); | 
					
						
							|  |  |  |     console.info(['Username', ...buildQueryAndParams('', date).labels].join(',')); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     for (const username of allOrgMembers) { | 
					
						
							|  |  |  |       const results = await graphql(buildQueryAndParams(username, date).query); | 
					
						
							|  |  |  |       const values = Object.values(results).map(result => `${result.issueCount}`); | 
					
						
							|  |  |  |       console.info([username, ...values].join(',')); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |   } catch (error) { | 
					
						
							|  |  |  |     console.error(`Error: ${error.message}`); | 
					
						
							|  |  |  |     process.exit(1); | 
					
						
							| 
									
										
										
										
											2020-03-03 10:16:18 -08:00
										 |  |  |   } | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | run(args['since']); |