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

@ -13,33 +13,45 @@ interface APIUser {
token: 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. * Describes the results from an issue lookup.
*/ */
interface IssueResults { interface IssueResults {
totalIssues: number, totalIssues: number;
cursor: string, cursor: string;
issues: { id: string, title: string }[]; issues: { id: string; title: string }[];
} }
export default class TransferIssues extends Command { export default class TransferIssues extends Command {
static description = 'Transfers issues from another repository into the monorepo.'; static description =
'Transfers issues from another repository into the monorepo.';
static args = [ static args = [
{ {
name: 'source', name: 'source',
description: 'The GitHub repository we are transferring from.', description: 'The GitHub repository we are transferring from.',
required: true, required: true,
} },
]; ];
static flags = { static flags = {
filter: Flags.string( filter: Flags.string( {
{ description:
description: 'A search filter to apply when searching for issues to transfer.', 'A search filter to apply when searching for issues to transfer.',
default: 'is:open' default: 'is:open',
} } ),
) labels: Flags.string( {
description:
'A label that should be added to the issue post-migration.',
multiple: true,
} ),
}; };
/** /**
@ -50,42 +62,67 @@ interface IssueResults {
this.validateArgs( args.source ); 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 );
}
CliUx.ux.action.start('Validating API arguments');
const apiUser = await this.getAPIUser(); const apiUser = await this.getAPIUser();
const issueChanges = await this.checkAPIArguments( apiUser, args.source, flags.labels );
let confirmation = await CliUx.ux.confirm('Are you sure you want to transfer issues from ' + args.source + ' into the monorepo? (y/n)' ); CliUx.ux.action.stop();
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 );
}
// Iterate over all of the issues and transfer them to the monorepo. // Iterate over all of the issues and transfer them to the monorepo.
let cursor: string | null = null; let cursor: string | null = null;
let totalTransferred = 0; let totalTransferred = 0;
let totalIssues = null; let totalIssues = 0;
do { do {
const issues: IssueResults = await this.loadIssues( apiUser, args.source, flags.filter, cursor ); const issues: IssueResults = await this.loadIssues(
apiUser,
args.source,
flags.filter,
cursor
);
if ( issues.issues.length === 0 ) { if ( issues.issues.length === 0 ) {
break; break;
} }
if (totalIssues === null) { if ( totalIssues === 0 ) {
totalIssues = issues.totalIssues; totalIssues = issues.totalIssues;
confirmation = await CliUx.ux.confirm('This will transfer ' + totalIssues + ' issues, are you sure? (y/n)' ); confirmation = await CliUx.ux.confirm(
'This will transfer ' +
totalIssues +
' issues. There is no command to reverse this, are you sure? (y/n)'
);
if ( ! confirmation ) { if ( ! confirmation ) {
this.exit( 0 ); this.exit( 0 );
} }
} }
totalTransferred += await this.transferIssues(apiUser, issues); totalTransferred += await this.transferIssues(
apiUser,
issueChanges,
issues
);
cursor = issues.cursor; cursor = issues.cursor;
} while (cursor !== null) {} } while ( cursor !== null );
{
}
this.log( 'Successfully transferred ' + totalTransferred + '/' + totalIssues + ' issues.' ); this.log(
'Successfully transferred ' +
totalTransferred +
'/' +
totalIssues +
' issues.'
);
} }
/** /**
@ -107,37 +144,37 @@ interface IssueResults {
*/ */
private async getAPIUser(): Promise< APIUser > { 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. // 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 }); const token: string = await CliUx.ux.prompt(
'Please supply a GitHub API token',
{ type: 'hide', required: true }
);
if ( token === '' ) { if ( token === '' ) {
this.error( 'You must enter a valid GitHub API token' ); this.error( 'You must enter a valid GitHub API token' );
} }
try { try {
const { viewer } = await graphql( const { viewer } = await graphql( '{ viewer { id } }', {
'{ viewer { id } }',
{
headers: { headers: {
authorization: 'token ' + token authorization: 'token ' + token,
} },
} } );
);
const { repository } = await graphql( const { repository } = await graphql(
'{ repository (owner: "woocommerce", name: "woocommerce" ) { id } }', '{ repository (owner: "woocommerce", name: "woocommerce" ) { id } }',
{ {
headers: { headers: {
authorization: 'token ' + token authorization: 'token ' + token,
} },
} }
); );
return { return {
id: viewer.id, id: viewer.id,
monorepoID: repository.id, monorepoID: repository.id,
token token,
}; };
} catch ( err: any ) { } catch ( err: any ) {
if ( err?.status == 401 ) { if ( err?.status === 401 ) {
this.error( 'The given token is invalid' ); this.error( 'The given token is invalid' );
} }
@ -145,6 +182,90 @@ interface IssueResults {
} }
} }
/**
* 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
}
}
}
}
`,
{
headers: {
authorization: 'token ' + apiUser.token,
},
}
);
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 ] );
}
}
return changes;
}
/** /**
* Loads a set of issues from the * Loads a set of issues from the
* *
@ -153,7 +274,12 @@ interface IssueResults {
* @param {string} filter The search filter for the issue search. * @param {string} filter The search filter for the issue search.
* @param {string|null} cursor The cursor for the current in-progress 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 > { private async loadIssues(
apiUser: APIUser,
source: string,
filter: string,
cursor: string | null
): Promise< IssueResults > {
const cursorString = cursor ? ', after: "' + cursor + '"' : ''; const cursorString = cursor ? ', after: "' + cursor + '"' : '';
const { search } = await graphql( const { search } = await graphql(
@ -175,24 +301,24 @@ interface IssueResults {
`, `,
{ {
headers: { headers: {
authorization: 'token ' + apiUser.token authorization: 'token ' + apiUser.token,
} },
} }
); );
const nextCursor = search.pageInfo.endCursor; const nextCursor = search.pageInfo.endCursor;
const issues: { id: string, title: string }[] = []; const issues: { id: string; title: string }[] = [];
for ( const issue of search.nodes ) { for ( const issue of search.nodes ) {
issues.push( { issues.push( {
id: issue.id, id: issue.id,
title: issue.title title: issue.title,
} ); } );
} }
return { return {
totalIssues: search.issueCount, totalIssues: search.issueCount,
cursor: nextCursor, cursor: nextCursor,
issues: issues issues,
}; };
} }
@ -200,9 +326,14 @@ interface IssueResults {
* Transfers a set of issues to the monorepo. * Transfers a set of issues to the monorepo.
* *
* @param {APIUser} apiUser The API user making the transfer request. * @param {APIUser} apiUser The API user making the transfer request.
* @param {IssueResult} issues The issues to be transferred to the monorepo. * @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, issues: IssueResults ): Promise< number > { private async transferIssues(
apiUser: APIUser,
issueChanges: IssueChanges,
issues: IssueResults
): Promise< number > {
// Track the number of issues so that we can keep up with them. // Track the number of issues so that we can keep up with them.
let issuesTransferred = 0; let issuesTransferred = 0;
for ( const issue of issues.issues ) { for ( const issue of issues.issues ) {
@ -215,4 +346,3 @@ interface IssueResults {
return issuesTransferred; return issuesTransferred;
} }
} }