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:
parent
831a895db3
commit
1519eeac81
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue