2020-03-03 13:16:18 -05:00
|
|
|
/**
|
|
|
|
* @license
|
|
|
|
* Copyright Google Inc. All Rights Reserved.
|
|
|
|
*
|
|
|
|
* 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;
|
|
|
|
}
|
|
|
|
return members;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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`]: {
|
|
|
|
query:
|
|
|
|
`repo:${ORG}/${repo} is:issue -author:${username} involves:${username} ${updatedOrCreated}:>${date}`,
|
|
|
|
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`]: {
|
|
|
|
query:
|
|
|
|
`repo:${ORG}/${repo} is:pr -author:${username} reviewed-by:${username} ${updatedOrCreated}:>${date}`,
|
|
|
|
label: `${ORG}/${repo} PR Reviewed`,
|
|
|
|
},
|
|
|
|
[`${repo.replace(/[\/\-]/g, '_')}_pr_commented`]: {
|
|
|
|
query:
|
|
|
|
`repo:${ORG}/${repo} is:pr -author:${username} commenter:${username} ${updatedOrCreated}:>${date}`,
|
|
|
|
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`]: {
|
|
|
|
query:
|
|
|
|
`org:${ORG} is:issue -author:${username} involves:${username} ${updatedOrCreated}:>${date}`,
|
|
|
|
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`]: {
|
|
|
|
query:
|
|
|
|
`org:${ORG} is:pr -author:${username} reviewed-by:${username} ${updatedOrCreated}:>${date}`,
|
|
|
|
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 04:01:44 -04: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 13:16:18 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
run(args['since']);
|