angular-cn/tools/contributing-stats/get-data.ts

251 lines
8.2 KiB
TypeScript

/**
* @license
* Copyright Google LLC 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.sort();
}
/**
* 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) {
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);
}
}
run(args['since']);