angular-cn/dev-infra/caretaker/check/github.ts

121 lines
3.8 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
*/
import {alias, onUnion, params, types} from 'typed-graphqlify';
import {bold, debug, info} from '../../utils/console';
import {CaretakerConfig} from '../config';
import {BaseModule} from './base';
/** A list of generated results for a github query. */
type GithubQueryResults = {
queryName: string,
count: number,
queryUrl: string,
matchedUrls: string[],
}[];
/** The fragment for a result from Github's api for a Github query. */
const GithubQueryResultFragment = {
issueCount: types.number,
nodes: [{...onUnion({
PullRequest: {
url: types.string,
},
Issue: {
url: types.string,
},
})}],
};
/** An object containing results of multiple queries. */
type GithubQueryResult = {
[key: string]: typeof GithubQueryResultFragment;
};
/**
* Cap the returned issues in the queries to an arbitrary 20. At that point, caretaker has a lot
* of work to do and showing more than that isn't really useful.
*/
const MAX_RETURNED_ISSUES = 20;
export class GithubQueriesModule extends BaseModule<GithubQueryResults|void> {
async retrieveData() {
// Non-null assertion is used here as the check for undefined immediately follows to confirm the
// assertion. Typescript's type filtering does not seem to work as needed to understand
// whether githubQueries is undefined or not.
let queries = this.config.caretaker?.githubQueries!;
if (queries === undefined || queries.length === 0) {
debug('No github queries defined in the configuration, skipping');
return;
}
/** The results of the generated github query. */
const queryResult = await this.git.github.graphql.query(this.buildGraphqlQuery(queries));
const results = Object.values(queryResult);
const {owner, name: repo} = this.git.remoteConfig;
return results.map((result, i) => {
return {
queryName: queries[i].name,
count: result.issueCount,
queryUrl: encodeURI(`https://github.com/${owner}/${repo}/issues?q=${queries[i].query}`),
matchedUrls: result.nodes.map(node => node.url)
};
});
}
/** Build a Graphql query statement for the provided queries. */
private buildGraphqlQuery(queries: NonNullable<CaretakerConfig['githubQueries']>) {
/** The query object for graphql. */
const graphQlQuery: GithubQueryResult = {};
const {owner, name: repo} = this.git.remoteConfig;
/** The Github search filter for the configured repository. */
const repoFilter = `repo:${owner}/${repo}`;
queries.forEach(({name, query}) => {
/** The name of the query, with spaces removed to match GraphQL requirements. */
const queryKey = alias(name.replace(/ /g, ''), 'search');
graphQlQuery[queryKey] = params(
{
type: 'ISSUE',
first: MAX_RETURNED_ISSUES,
query: `"${repoFilter} ${query.replace(/"/g, '\\"')}"`,
},
{...GithubQueryResultFragment});
});
return graphQlQuery;
}
async printToTerminal() {
const queryResults = await this.data;
if (!queryResults) {
return;
}
info.group(bold('Github Tasks'));
const minQueryNameLength = Math.max(...queryResults.map(result => result.queryName.length));
for (const queryResult of queryResults) {
info(`${queryResult.queryName.padEnd(minQueryNameLength)} ${queryResult.count}`);
if (queryResult.count > 0) {
info.group(queryResult.queryUrl);
queryResult.matchedUrls.forEach(url => info(`- ${url}`));
if (queryResult.count > MAX_RETURNED_ISSUES) {
info(`... ${queryResult.count - MAX_RETURNED_ISSUES} additional matches`);
}
info.groupEnd();
}
}
info.groupEnd();
info();
}
}