Added Label Flag

This commit adds a flag that enables the command to check
for labels that should be added to the issues after they
are transferred into the monorepo.
This commit is contained in:
Christopher Allford 2022-03-16 23:29:17 -07:00
parent 831a895db3
commit 1519eeac81
1 changed files with 284 additions and 154 deletions

View File

@ -1,96 +1,133 @@
/**
* External dependencies
*/
import { CliUx, Command, Flags } from '@oclif/core';
/**
* External dependencies
*/
import { CliUx, Command, Flags } from '@oclif/core';
import { graphql, GraphqlResponseError } from '@octokit/graphql';
/**
* Describes the information for a user that the command needs to operate.
*/
interface APIUser {
id: string;
monorepoID: string;
token: string;
id: string;
monorepoID: string;
token: string;
}
/**
* Describes the changes we want to make to the issues after we transfer them.
*/
interface IssueChanges {
addLabelIDs: string[];
}
/**
* Describes the results from an issue lookup.
*/
interface IssueResults {
totalIssues: number,
cursor: string,
issues: { id: string, title: string }[];
totalIssues: number;
cursor: string;
issues: { id: string; title: string }[];
}
export default class TransferIssues extends Command {
static description = 'Transfers issues from another repository into the monorepo.';
static args = [
{
name: 'source',
description: 'The GitHub repository we are transferring from.',
required: true,
}
];
static flags = {
filter: Flags.string(
{
description: 'A search filter to apply when searching for issues to transfer.',
default: 'is:open'
}
)
export default class TransferIssues extends Command {
static description =
'Transfers issues from another repository into the monorepo.';
static args = [
{
name: 'source',
description: 'The GitHub repository we are transferring from.',
required: true,
},
];
static flags = {
filter: Flags.string( {
description:
'A search filter to apply when searching for issues to transfer.',
default: 'is:open',
} ),
labels: Flags.string( {
description:
'A label that should be added to the issue post-migration.',
multiple: true,
} ),
};
/**
* This method is called to execute the command.
*/
async run(): Promise< void > {
const { args, flags } = await this.parse( TransferIssues );
this.validateArgs( args.source );
/**
* This method is called to execute the command.
*/
async run(): Promise< void > {
const { args, flags } = await this.parse( TransferIssues );
const apiUser = await this.getAPIUser();
this.validateArgs( args.source );
let confirmation = await CliUx.ux.confirm('Are you sure you want to transfer issues from ' + args.source + ' into the monorepo? (y/n)' );
if (!confirmation) {
this.exit( 0 );
}
let confirmation = await CliUx.ux.confirm(
'Are you sure you want to transfer issues from ' +
args.source +
' into the monorepo? (y/n)'
);
if ( ! confirmation ) {
this.exit( 0 );
}
confirmation = await CliUx.ux.confirm('This command CANNOT reverse the transfer, are you sure? (y/n)' );
if (!confirmation) {
this.exit( 0 );
}
CliUx.ux.action.start('Validating API arguments');
// Iterate over all of the issues and transfer them to the monorepo.
let cursor: string | null = null;
let totalTransferred = 0;
let totalIssues = null;
do {
const issues: IssueResults = await this.loadIssues( apiUser, args.source, flags.filter, cursor );
if (issues.issues.length === 0) {
break;
}
const apiUser = await this.getAPIUser();
const issueChanges = await this.checkAPIArguments( apiUser, args.source, flags.labels );
if (totalIssues === null) {
totalIssues = issues.totalIssues;
CliUx.ux.action.stop();
confirmation = await CliUx.ux.confirm('This will transfer ' + totalIssues + ' issues, are you sure? (y/n)' );
if (!confirmation) {
this.exit( 0 );
}
}
// Iterate over all of the issues and transfer them to the monorepo.
let cursor: string | null = null;
let totalTransferred = 0;
let totalIssues = 0;
do {
const issues: IssueResults = await this.loadIssues(
apiUser,
args.source,
flags.filter,
cursor
);
if ( issues.issues.length === 0 ) {
break;
}
totalTransferred += await this.transferIssues(apiUser, issues);
cursor = issues.cursor;
} while (cursor !== null) {}
if ( totalIssues === 0 ) {
totalIssues = issues.totalIssues;
this.log( 'Successfully transferred ' + totalTransferred + '/' + totalIssues + ' issues.' );
}
confirmation = await CliUx.ux.confirm(
'This will transfer ' +
totalIssues +
' issues. There is no command to reverse this, are you sure? (y/n)'
);
if ( ! confirmation ) {
this.exit( 0 );
}
}
/**
totalTransferred += await this.transferIssues(
apiUser,
issueChanges,
issues
);
cursor = issues.cursor;
} while ( cursor !== null );
{
}
this.log(
'Successfully transferred ' +
totalTransferred +
'/' +
totalIssues +
' issues.'
);
}
/**
* Validates all of the arguments to make sure they're compatible with the command.
*
*
* @param {string} source The GitHub repository we are transferring from.
*/
private validateArgs( source: string ): void {
@ -102,64 +139,153 @@ interface IssueResults {
}
}
/**
* Requests an API token from the user, validates it, and returns information about them if successful.
*/
private async getAPIUser(): Promise< APIUser > {
// Prompt them for a token, rather than storing one. This reduces the likelihood that the command can be accidentally executed.
const token: string = await CliUx.ux.prompt( 'Please supply a GitHub API token', { type: 'hide', required: true });
if (token === '') {
this.error( 'You must enter a valid GitHub API token' );
}
/**
* Requests an API token from the user, validates it, and returns information about them if successful.
*/
private async getAPIUser(): Promise< APIUser > {
// Prompt them for a token, rather than storing one. This reduces the likelihood that the command can be accidentally executed.
const token: string = await CliUx.ux.prompt(
'Please supply a GitHub API token',
{ type: 'hide', required: true }
);
if ( token === '' ) {
this.error( 'You must enter a valid GitHub API token' );
}
try {
const { viewer } = await graphql(
'{ viewer { id } }',
{
headers: {
authorization: 'token ' + token
try {
const { viewer } = await graphql( '{ viewer { id } }', {
headers: {
authorization: 'token ' + token,
},
} );
const { repository } = await graphql(
'{ repository (owner: "woocommerce", name: "woocommerce" ) { id } }',
{
headers: {
authorization: 'token ' + token,
},
}
);
return {
id: viewer.id,
monorepoID: repository.id,
token,
};
} catch ( err: any ) {
if ( err?.status === 401 ) {
this.error( 'The given token is invalid' );
}
throw err;
}
}
/**
* Checks the arguments that will be sent to the GitHub API for validity.
*
* @param {APIUser} apiUser The API user that is making the transfer request.
* @param {string} source The GitHub repository we are transferring issues from.
* @param {Array.<string>} labels The labels to be applied to the issues post-transfer.
*/
private async checkAPIArguments(
apiUser: APIUser,
source: string,
labels: string[]
): Promise< IssueChanges > {
const changes: IssueChanges = {
addLabelIDs: []
};
const [ owner, name ] = source.split( '/' );
try {
await graphql(
`{ repository (owner: "${ owner }", name: "${ name }" ) { id } }`,
{
headers: {
authorization: 'token ' + apiUser.token,
},
}
);
} catch {
this.error( 'Unable to find repository ' + source );
}
// Paginate all of the labels in the repository to check against the input.
if (labels && labels.length > 0) {
const allLabels: { [ key: string ]: string } = {};
let cursor: string | null = null;
do {
const cursorString: string = cursor
? ', after: "' + cursor + '"'
: '';
const { repository } = await graphql(
`
{
repository (owner: "woocommerce", name: "woocommerce" ) {
labels (first: 10${ cursorString }) {
nodes {
id,
name
},
pageInfo {
endCursor
}
}
}
}
}
);
const { repository } = await graphql(
'{ repository (owner: "woocommerce", name: "woocommerce" ) { id } }',
{
headers: {
authorization: 'token ' + token
`,
{
headers: {
authorization: 'token ' + apiUser.token,
},
}
}
);
);
return {
id: viewer.id,
monorepoID: repository.id,
token
};
} catch ( err: any ) {
if ( err?.status == 401 ) {
this.error( 'The given token is invalid' );
if ( repository.labels.nodes.length === 0 ) {
break;
}
cursor = repository.labels.pageInfo.endCursor;
for ( const label of repository.labels.nodes ) {
allLabels[ label.name ] = label.id;
}
} while ( cursor !== null );
for (const label of labels) {
if ( ! allLabels[ label ] ) {
this.error( 'The monorepo does not have the label ' + label + '.' );
}
changes.addLabelIDs.push( allLabels[ label ] );
}
throw err;
}
}
/**
* Loads a set of issues from the
*
* @param {APIUser} apiUser The API user that is making the transfer request.
* @param {string} source The GitHub repository we are transferring issues from.
* @param {string} filter The search filter for the issue search.
* @param {string|null} cursor The cursor for the current in-progress issue search.
*/
private async loadIssues( apiUser: APIUser, source: string, filter: string, cursor: string | null ): Promise< IssueResults > {
const cursorString = cursor ? ', after: "' + cursor + '"' : '';
return changes;
}
const { search } = await graphql(
`
/**
* Loads a set of issues from the
*
* @param {APIUser} apiUser The API user that is making the transfer request.
* @param {string} source The GitHub repository we are transferring issues from.
* @param {string} filter The search filter for the issue search.
* @param {string|null} cursor The cursor for the current in-progress issue search.
*/
private async loadIssues(
apiUser: APIUser,
source: string,
filter: string,
cursor: string | null
): Promise< IssueResults > {
const cursorString = cursor ? ', after: "' + cursor + '"' : '';
const { search } = await graphql(
`
{
search(type: ISSUE, query: "repo:${source} is:issue ${filter}", first: 50${cursorString}) {
search(type: ISSUE, query: "repo:${ source } is:issue ${ filter }", first: 50${ cursorString }) {
nodes {
... on Issue {
id,
@ -173,46 +299,50 @@ interface IssueResults {
}
}
`,
{
headers: {
authorization: 'token ' + apiUser.token
}
}
);
{
headers: {
authorization: 'token ' + apiUser.token,
},
}
);
const nextCursor = search.pageInfo.endCursor;
const issues: { id: string, title: string }[] = [];
for ( const issue of search.nodes ) {
issues.push({
id: issue.id,
title: issue.title
});
}
const nextCursor = search.pageInfo.endCursor;
const issues: { id: string; title: string }[] = [];
for ( const issue of search.nodes ) {
issues.push( {
id: issue.id,
title: issue.title,
} );
}
return {
totalIssues: search.issueCount,
cursor: nextCursor,
issues: issues
};
}
return {
totalIssues: search.issueCount,
cursor: nextCursor,
issues,
};
}
/**
* Transfers a set of issues to the monorepo.
*
* @param {APIUser} apiUser The API user making the transfer request.
* @param {IssueResult} issues The issues to be transferred to the monorepo.
*/
private async transferIssues( apiUser: APIUser, issues: IssueResults ): Promise< number > {
// Track the number of issues so that we can keep up with them.
let issuesTransferred = 0;
for (const issue of issues.issues) {
CliUx.ux.action.start( 'Transferring "' + issue.title + '"' );
/**
* Transfers a set of issues to the monorepo.
*
* @param {APIUser} apiUser The API user making the transfer request.
* @param {IssueChanges} issueChanges The changes we should make to the issues during the transfer.
* @param {IssueResults} issues The issues to be transferred to the monorepo.
*/
private async transferIssues(
apiUser: APIUser,
issueChanges: IssueChanges,
issues: IssueResults
): Promise< number > {
// Track the number of issues so that we can keep up with them.
let issuesTransferred = 0;
for ( const issue of issues.issues ) {
CliUx.ux.action.start( 'Transferring "' + issue.title + '"' );
issuesTransferred++;
CliUx.ux.action.stop();
}
issuesTransferred++;
CliUx.ux.action.stop();
}
return issuesTransferred;
}
}
return issuesTransferred;
}
}