Added Project Field Migration (#41970)

This will ensure that we are also transferring the project data.
This commit is contained in:
Christopher Allford 2023-12-08 15:52:27 -08:00 committed by GitHub
parent 1128401778
commit eb1abbbb25
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
1 changed files with 239 additions and 46 deletions

View File

@ -5,12 +5,6 @@ import { CliUx, Command, Flags } from '@oclif/core';
import { graphql, GraphqlResponseError } from '@octokit/graphql';
import { RequestError } from '@octokit/request-error';
/**
* Make sure we aren't hardcoding the monorepo into the command.
*/
const MONOREPO_OWNER = 'woocommerce';
const MONOREPO_NAME = 'woocommerce';
/**
* Described a label object.
*/
@ -19,17 +13,34 @@ interface GitHubLabel {
name: string;
}
// These properties should match the UpdateProjectV2ItemFieldValueInput mutation input.
interface GitHubProjectField {
projectId: string;
fieldId: string;
itemId: string;
dataType: string;
value: {
text?: string;
number?: number;
date?: string;
singleSelectOptionId?: string;
iterationId?: string;
};
}
/**
* Describes an issue object.
*/
interface GitHubIssue {
id: string;
newID?: string;
title: string;
projectFields: GitHubProjectField[];
}
export default class TransferIssues extends Command {
static description =
'Transfers issues from another repository into the monorepo.';
'Transfers issues from another repository into the destination repository.';
static args = [
{
@ -40,6 +51,11 @@ export default class TransferIssues extends Command {
];
static flags = {
destination: Flags.string( {
description:
'The destination repository to transfer into.',
default: 'woocommerce/woocommerce',
} ),
searchFilter: Flags.string( {
description:
'The search filter to apply when searching for issues to transfer.',
@ -57,6 +73,22 @@ export default class TransferIssues extends Command {
async run(): Promise< void > {
const { args, flags } = await this.parse( TransferIssues );
const matches = flags.destination.match( /^([^/]+)\/([^/]+)$/ );
if ( ! matches ) {
this.error(
'The destination repository must be in the format "owner/repo"!'
);
}
const destinationOwner = matches[1];
const destinationRepo = matches[2];
if ( ! args.source.startsWith( destinationOwner + '/' ) ) {
this.error(
'The source and target repository must have the same owner!'
);
}
let confirmation = await CliUx.ux.confirm(
'Are you sure you want to transfer issues from ' +
args.source +
@ -91,10 +123,14 @@ export default class TransferIssues extends Command {
}
const monorepoNodeID = await this.getMonorepoNodeID(
authenticatedGraphQL
authenticatedGraphQL,
destinationOwner,
destinationRepo
);
const labelsToAdd = await this.getLabelsToAdd(
authenticatedGraphQL,
destinationOwner,
destinationRepo,
flags.labels
);
const issuesToTransfer = await this.getIssues(
@ -103,7 +139,7 @@ export default class TransferIssues extends Command {
flags.searchFilter
);
const newIssueIDs: string[] = [];
let transferredIssues = 0;
for ( const issue of issuesToTransfer ) {
const newIssueID = await this.transferIssue(
authenticatedGraphQL,
@ -111,8 +147,10 @@ export default class TransferIssues extends Command {
monorepoNodeID
);
if ( newIssueID !== null ) {
newIssueIDs.push( newIssueID );
// Track the transferred issue.
if ( newIssueID ) {
issue.newID = newIssueID;
transferredIssues++;
}
}
@ -123,11 +161,20 @@ export default class TransferIssues extends Command {
await new Promise( ( resolve ) => setTimeout( resolve, 5000 ) );
CliUx.ux.action.stop();
CliUx.ux.action.start( 'Applying label changes' );
for ( const newIssueID of newIssueIDs ) {
CliUx.ux.action.start( 'Running post-transfer tasks' );
for ( const issue of issuesToTransfer ) {
if ( ! issue.newID ) {
continue;
}
this.updateProjectFields(
authenticatedGraphQL,
issue.projectFields
);
this.addLabelsToIssue(
authenticatedGraphQL,
newIssueID,
issue.newID,
labelsToAdd
);
}
@ -135,7 +182,7 @@ export default class TransferIssues extends Command {
this.log(
'Successfully transferred ' +
newIssueIDs.length +
transferredIssues +
'/' +
numberOfIssues +
' issues.'
@ -184,9 +231,13 @@ export default class TransferIssues extends Command {
* Fetches the node ID of the monorepo from GitHub.
*
* @param {graphql} authenticatedGraphQL The graphql object for making requests.
* @param {string} destinationOwner The owner of the repository to transfer issues into.
* @param {string} destinationRepo The repository to transfer issues into.
*/
private async getMonorepoNodeID(
authenticatedGraphQL: typeof graphql
authenticatedGraphQL: typeof graphql,
destinationOwner: string,
destinationRepo: string,
): Promise< string > {
CliUx.ux.action.start( 'Finding Monorepo' );
@ -206,8 +257,8 @@ export default class TransferIssues extends Command {
}
`,
{
monorepoOwner: MONOREPO_OWNER,
monorepoName: MONOREPO_NAME,
monorepoOwner: destinationOwner,
monorepoName: destinationRepo,
}
);
@ -219,11 +270,7 @@ export default class TransferIssues extends Command {
if ( err instanceof GraphqlResponseError ) {
this.error(
'Could not find the repository "' +
MONOREPO_OWNER +
'/' +
MONOREPO_NAME +
'"'
'Could not find the repository "' + destinationOwner + '/' + destinationRepo + '"'
);
}
@ -237,10 +284,14 @@ export default class TransferIssues extends Command {
* Gets all of the labels we want to add from GitHub.
*
* @param {graphql} authenticatedGraphQL The graphql object for making requests.
* @param {string} destinationOwner The owner of the repository to transfer issues into.
* @param {string} destinationRepo The repository to transfer issues into.
* @param {Array.<GitHubLabel>} labels The labels we want to add after the transfer.
*/
private async getLabelsToAdd(
authenticatedGraphQL: typeof graphql,
destinationOwner: string,
destinationRepo: string,
labels?: string
): Promise< GitHubLabel[] > {
if ( ! labels ) {
@ -281,8 +332,8 @@ export default class TransferIssues extends Command {
}
`,
{
monorepoOwner: MONOREPO_OWNER,
monorepoName: MONOREPO_NAME,
monorepoOwner: destinationOwner,
monorepoName: destinationRepo,
cursor,
}
);
@ -305,7 +356,7 @@ export default class TransferIssues extends Command {
for ( const label of addLabels ) {
if ( ! allLabels[ label ] ) {
this.error(
'The monorepo does not have the label ' + label + '.'
'The target repository does not have the label ' + label + '.'
);
}
@ -384,26 +435,83 @@ export default class TransferIssues extends Command {
};
} >(
`
query ( $searchQuery: String!, $cursor: String ) {
search (
type: ISSUE,
query: $searchQuery,
first: 100,
after: $cursor
) {
nodes {
... on Issue {
id,
title
}
},
pageInfo {
hasNextPage,
endCursor
}
}
}
`,
query ($searchQuery: String!, $cursor: String) {
search(type: ISSUE, query: $searchQuery, first: 100, after: $cursor) {
nodes {
... on Issue {
id
title
projectItems(first: 50) {
nodes {
id
project {
id
}
fieldValues(first: 50) {
nodes {
... on ProjectV2ItemFieldTextValue {
field {
... on ProjectV2FieldCommon {
id
name
dataType
}
}
text
}
... on ProjectV2ItemFieldNumberValue {
field {
... on ProjectV2FieldCommon {
id
name
dataType
}
}
number
}
... on ProjectV2ItemFieldDateValue {
field {
... on ProjectV2FieldCommon {
id
name
dataType
}
}
date
}
... on ProjectV2ItemFieldSingleSelectValue {
field {
... on ProjectV2FieldCommon {
id
name
dataType
}
}
optionId
}
... on ProjectV2ItemFieldIterationValue {
field {
... on ProjectV2FieldCommon {
id
name
dataType
}
}
iterationId
}
}
}
}
}
}
}
pageInfo {
hasNextPage
endCursor
}
}
}
`,
{
searchQuery,
cursor,
@ -412,9 +520,58 @@ export default class TransferIssues extends Command {
// Record all of the issues that we've found
for ( const issue of search.nodes ) {
const projectFields: GitHubProjectField[] = [];
// @ts-expect-error The types do not cover the projectItems field.
for ( const projectItem of issue.projectItems.nodes ) {
for ( const fieldValue of projectItem.fieldValues.nodes ) {
// We only care if this is a field we can process.
if ( ! fieldValue.field ) {
continue;
}
const projectField: GitHubProjectField = {
projectId: projectItem.project.id,
fieldId: fieldValue.field.id,
itemId: projectItem.id,
dataType: fieldValue.field.dataType,
value: {},
};
// Map the field value to the correct property.
switch ( fieldValue.field.dataType ) {
case 'TEXT':
projectField.value = { text: fieldValue.text };
break;
case 'NUMBER':
projectField.value = { number: fieldValue.number };
break;
case 'DATE':
projectField.value = { date: fieldValue.date };
break;
case 'SINGLE_SELECT':
projectField.value = { singleSelectOptionId: fieldValue.optionId };
break;
case 'ITERATION':
projectField.value = { iterationId: fieldValue.iterationId };
break;
default:
continue;
}
projectFields.push( projectField );
}
}
issues.push( {
id: issue.id,
title: issue.title,
projectFields: projectFields
} );
}
@ -483,6 +640,42 @@ export default class TransferIssues extends Command {
}
}
/**
* Update the project fields for the issue.
*
* @param {graphql} authenticatedGraphQL The graphql object for making requests.
* @param {Array.<GitHubLabel>} projectFields The project fields to update for the issue.
*/
private async updateProjectFields(
authenticatedGraphQL: typeof graphql,
projectFields: GitHubProjectField[]
) {
for ( const projectField of projectFields ) {
const input = {
clientMutationId: 'monorepo-merge',
projectId: projectField.projectId,
fieldId: projectField.fieldId,
itemId: projectField.itemId,
value: projectField.value,
};
await authenticatedGraphQL(
`
mutation ( $input: UpdateProjectV2ItemFieldValueInput! ) {
updateProjectV2ItemFieldValue (
input: $input
) {
projectV2Item {
id
}
}
}
`,
{ input }
);
}
}
/**
* Adds labels to an issue.
*