Raw File
get-data.ts
/**
 * @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 {alias, params, query as graphqlQuery, types} from 'typed-graphqlify';
import yargs from 'yargs';

// 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 = yargs.option('use-created', {type: 'boolean'})
                 .option('since', {type: 'string', demandOption: true})
                 .strictOptions()
                 .argv;

/**
 * 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.toString(), 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.toString());
      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']);
back to top