Added Issue Transfer Command Skeleton
This commit adds the skeleton for an issue transfer command. Almost all of the functionality is implemented, but it is missing the actual transfer of issues. I'm nervous about testing this, so I'm going to implement the rest of the functionality first.
This commit is contained in:
parent
1a29e3861c
commit
57ab538668
|
@ -382,6 +382,7 @@ importers:
|
|||
'@oclif/core': ^1
|
||||
'@oclif/plugin-help': ^5
|
||||
'@oclif/plugin-plugins': ^2.0.1
|
||||
'@octokit/graphql': 4.8.0
|
||||
'@types/node': ^16.9.4
|
||||
eslint: ^7.32.0
|
||||
globby: ^11
|
||||
|
@ -394,6 +395,7 @@ importers:
|
|||
'@oclif/core': 1.3.4
|
||||
'@oclif/plugin-help': 5.1.11
|
||||
'@oclif/plugin-plugins': 2.1.0
|
||||
'@octokit/graphql': 4.8.0
|
||||
devDependencies:
|
||||
'@types/node': 16.10.3
|
||||
eslint: 7.32.0
|
||||
|
@ -4025,7 +4027,6 @@ packages:
|
|||
'@octokit/types': 6.34.0
|
||||
is-plain-object: 5.0.0
|
||||
universal-user-agent: 6.0.0
|
||||
dev: true
|
||||
|
||||
/@octokit/graphql/4.8.0:
|
||||
resolution: {integrity: sha512-0gv+qLSBLKF0z8TKaSKTsS39scVKF9dbMxJpj3U0vC7wjNWFuIpL/z76Qe2fiuCbDRcJSavkXsVtMS6/dtQQsg==}
|
||||
|
@ -4035,11 +4036,9 @@ packages:
|
|||
universal-user-agent: 6.0.0
|
||||
transitivePeerDependencies:
|
||||
- encoding
|
||||
dev: true
|
||||
|
||||
/@octokit/openapi-types/11.2.0:
|
||||
resolution: {integrity: sha512-PBsVO+15KSlGmiI8QAzaqvsNlZlrDlyAJYcrXBCvVUxCp7VnXjkwPoFHgjEJXx3WF9BAwkA6nfCUA7i9sODzKA==}
|
||||
dev: true
|
||||
|
||||
/@octokit/plugin-paginate-rest/2.17.0_@octokit+core@3.5.1:
|
||||
resolution: {integrity: sha512-tzMbrbnam2Mt4AhuyCHvpRkS0oZ5MvwwcQPYGtMv4tUa5kkzG58SVB0fcsLulOZQeRnOgdkZWkRUiyBlh0Bkyw==}
|
||||
|
@ -4074,7 +4073,6 @@ packages:
|
|||
'@octokit/types': 6.34.0
|
||||
deprecation: 2.3.1
|
||||
once: 1.4.0
|
||||
dev: true
|
||||
|
||||
/@octokit/request/5.6.3:
|
||||
resolution: {integrity: sha512-bFJl0I1KVc9jYTe9tdGGpAMPy32dLBXXo1dS/YwSCTL/2nd9XeHsY616RE3HPXDVk+a+dBuzyz5YdlXwcDTr2A==}
|
||||
|
@ -4087,7 +4085,6 @@ packages:
|
|||
universal-user-agent: 6.0.0
|
||||
transitivePeerDependencies:
|
||||
- encoding
|
||||
dev: true
|
||||
|
||||
/@octokit/rest/18.12.0:
|
||||
resolution: {integrity: sha512-gDPiOHlyGavxr72y0guQEhLsemgVjwRePayJ+FcKc2SJqKUbxbkvf5kAZEWA/MKvsfYlQAMVzNJE3ezQcxMJ2Q==}
|
||||
|
@ -4104,7 +4101,6 @@ packages:
|
|||
resolution: {integrity: sha512-s1zLBjWhdEI2zwaoSgyOFoKSl109CUcVBCc7biPJ3aAf6LGLU6szDvi31JPU7bxfla2lqfhjbbg/5DdFNxOwHw==}
|
||||
dependencies:
|
||||
'@octokit/openapi-types': 11.2.0
|
||||
dev: true
|
||||
|
||||
/@parcel/watcher/2.0.4:
|
||||
resolution: {integrity: sha512-cTDi+FUDBIUOBKEtj+nhiJ71AZVlkAsQFuGQTun5tV9mwQBQgZvhCzG+URPQc8myeN32yRVZEfVAPCs1RW+Jvg==}
|
||||
|
@ -8821,7 +8817,6 @@ packages:
|
|||
|
||||
/deprecation/2.3.1:
|
||||
resolution: {integrity: sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==}
|
||||
dev: true
|
||||
|
||||
/des.js/1.0.1:
|
||||
resolution: {integrity: sha512-Q0I4pfFrv2VPd34/vfLrFOoRmlYj3OV50i7fskps1jZWK1kApMWWT9G6RRUeYedLcBDIhnSDaUvJMb3AhUlaEA==}
|
||||
|
@ -9128,6 +9123,7 @@ packages:
|
|||
|
||||
/encoding/0.1.13:
|
||||
resolution: {integrity: sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==}
|
||||
requiresBuild: true
|
||||
dependencies:
|
||||
iconv-lite: 0.6.3
|
||||
|
||||
|
@ -12779,7 +12775,6 @@ packages:
|
|||
/is-plain-object/5.0.0:
|
||||
resolution: {integrity: sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
dev: true
|
||||
|
||||
/is-potential-custom-element-name/1.0.1:
|
||||
resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==}
|
||||
|
@ -16133,7 +16128,6 @@ packages:
|
|||
optional: true
|
||||
dependencies:
|
||||
whatwg-url: 5.0.0
|
||||
dev: true
|
||||
|
||||
/node-forge/0.10.0:
|
||||
resolution: {integrity: sha512-PPmu8eEeG9saEUvI97fm4OYxXVB6bFvyNTyiUOBichBpFG8A1Ljw3bY62+5oOjDEMHRnd0Y7HQ+x7uzxOzC6JA==}
|
||||
|
@ -21134,7 +21128,6 @@ packages:
|
|||
|
||||
/universal-user-agent/6.0.0:
|
||||
resolution: {integrity: sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w==}
|
||||
dev: true
|
||||
|
||||
/universalify/0.1.2:
|
||||
resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==}
|
||||
|
|
|
@ -19,7 +19,8 @@
|
|||
"dependencies": {
|
||||
"@oclif/core": "^1",
|
||||
"@oclif/plugin-help": "^5",
|
||||
"@oclif/plugin-plugins": "^2.0.1"
|
||||
"@oclif/plugin-plugins": "^2.0.1",
|
||||
"@octokit/graphql": "4.8.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^16.9.4",
|
||||
|
@ -43,6 +44,9 @@
|
|||
"topics": {
|
||||
"merge": {
|
||||
"description": "Merges other repositories into the monorepo."
|
||||
},
|
||||
"transfer-issues": {
|
||||
"description": "Transfers issues from other repositories into the monorepo."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -95,7 +95,7 @@ export default class Merge extends Command {
|
|||
*/
|
||||
private async validateArgs( source: string, destination: string ): Promise< void > {
|
||||
// We only support pulling from GitHub so the format needs to match that.
|
||||
if ( ! source.match( /^[a-z0-9\-]+\/[a-z0-9\-]+$/ ) ) {
|
||||
if ( ! source.match( /^[a-zA-Z0-9\-_]+\/[a-zA-Z0-9\-_]+$/ ) ) {
|
||||
this.error(
|
||||
'The "source" argument must be in "organization/repository" format'
|
||||
);
|
||||
|
|
|
@ -0,0 +1,218 @@
|
|||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Describes the results from an issue lookup.
|
||||
*/
|
||||
interface IssueResults {
|
||||
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'
|
||||
}
|
||||
)
|
||||
};
|
||||
|
||||
/**
|
||||
* This method is called to execute the command.
|
||||
*/
|
||||
async run(): Promise< void > {
|
||||
const { args, flags } = await this.parse( TransferIssues );
|
||||
|
||||
this.validateArgs( args.source );
|
||||
|
||||
const apiUser = await this.getAPIUser();
|
||||
|
||||
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 );
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
if (totalIssues === null) {
|
||||
totalIssues = issues.totalIssues;
|
||||
|
||||
confirmation = await CliUx.ux.confirm('This will transfer ' + totalIssues + ' issues, are you sure? (y/n)' );
|
||||
if (!confirmation) {
|
||||
this.exit( 0 );
|
||||
}
|
||||
}
|
||||
|
||||
totalTransferred += await this.transferIssues(apiUser, 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 {
|
||||
// We only support pulling from GitHub so the format needs to match that.
|
||||
if ( ! source.match( /^[a-zA-Z0-9\-_]+\/[a-zA-Z0-9\-_]+$/ ) ) {
|
||||
this.error(
|
||||
'The "source" argument must be in "organization/repository" format'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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}) {
|
||||
nodes {
|
||||
... on Issue {
|
||||
id,
|
||||
title
|
||||
}
|
||||
},
|
||||
issueCount,
|
||||
pageInfo {
|
||||
endCursor
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
{
|
||||
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
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
totalIssues: search.issueCount,
|
||||
cursor: nextCursor,
|
||||
issues: 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 + '"' );
|
||||
|
||||
issuesTransferred++;
|
||||
CliUx.ux.action.stop();
|
||||
}
|
||||
|
||||
return issuesTransferred;
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue