Add replace label tool for use in GitHub Monorepo (#50649)
* Add some base tools for label updates * Add updated replace label script * Add state option * Rename folder * Include build file of folder rename * Update types and add comments * Update build * Remove unused option * Add check for existing label and address case sensitivity
This commit is contained in:
parent
a7f29ce98e
commit
f44c0b8064
File diff suppressed because one or more lines are too long
|
@ -2,6 +2,7 @@
|
||||||
* External dependencies
|
* External dependencies
|
||||||
*/
|
*/
|
||||||
import { Repository } from '@octokit/graphql-schema';
|
import { Repository } from '@octokit/graphql-schema';
|
||||||
|
import { Endpoints } from '@octokit/types';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
|
@ -39,6 +40,100 @@ export const getLatestGithubReleaseVersion = async ( options: {
|
||||||
).tagName;
|
).tagName;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getRepositoryLabel = async (
|
||||||
|
options: {
|
||||||
|
owner?: string;
|
||||||
|
name?: string;
|
||||||
|
},
|
||||||
|
label: string
|
||||||
|
): Promise<
|
||||||
|
Endpoints[ 'GET /repos/{owner}/{repo}/labels/{name}' ][ 'response' ][ 'data' ]
|
||||||
|
> => {
|
||||||
|
const { owner, name } = options;
|
||||||
|
try {
|
||||||
|
const { data } = await octokitWithAuth().request(
|
||||||
|
'GET /repos/{owner}/{repo}/labels/{label}',
|
||||||
|
{
|
||||||
|
owner,
|
||||||
|
repo: name,
|
||||||
|
label,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return data;
|
||||||
|
} catch ( e ) {
|
||||||
|
throw new Error( e );
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getIssuesByLabel = async (
|
||||||
|
options: {
|
||||||
|
owner?: string;
|
||||||
|
name?: string;
|
||||||
|
pageSize?: number;
|
||||||
|
},
|
||||||
|
label: string,
|
||||||
|
state: 'open' | 'closed' | 'all' = 'open'
|
||||||
|
): Promise< {
|
||||||
|
results: Endpoints[ 'GET /repos/{owner}/{repo}/issues' ][ 'response' ][ 'data' ];
|
||||||
|
} > => {
|
||||||
|
const { owner, name, pageSize } = options;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { data } = await octokitWithAuth().request(
|
||||||
|
'GET /repos/{owner}/{repo}/issues{?labels,state}',
|
||||||
|
{
|
||||||
|
owner,
|
||||||
|
repo: name,
|
||||||
|
labels: label,
|
||||||
|
per_page: pageSize || 100,
|
||||||
|
state,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
results: data,
|
||||||
|
};
|
||||||
|
} catch ( e ) {
|
||||||
|
throw new Error( e );
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateIssue = async (
|
||||||
|
options: {
|
||||||
|
owner?: string;
|
||||||
|
name?: string;
|
||||||
|
},
|
||||||
|
issueNumber: number,
|
||||||
|
updates: {
|
||||||
|
labels?: string[];
|
||||||
|
}
|
||||||
|
): Promise<
|
||||||
|
| Endpoints[ 'PATCH /repos/{owner}/{repo}/issues/{issue_number}' ][ 'response' ]
|
||||||
|
| false
|
||||||
|
> => {
|
||||||
|
const { owner, name } = options;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const branchOnGithub = await octokitWithAuth().request(
|
||||||
|
'PATCH /repos/{owner}/{repo}/issues/{issue_number}',
|
||||||
|
{
|
||||||
|
owner,
|
||||||
|
repo: name,
|
||||||
|
issue_number: issueNumber,
|
||||||
|
...updates,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return branchOnGithub;
|
||||||
|
} catch ( e ) {
|
||||||
|
if (
|
||||||
|
e.status === 404 &&
|
||||||
|
e.response.data.message === 'Issue not found'
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
throw new Error( e );
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export const doesGithubBranchExist = async (
|
export const doesGithubBranchExist = async (
|
||||||
options: {
|
options: {
|
||||||
owner?: string;
|
owner?: string;
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
# Github CLI Utility
|
||||||
|
|
||||||
|
CLI for performing Monorepo utilities relating to Github issues.
|
|
@ -0,0 +1,15 @@
|
||||||
|
/**
|
||||||
|
* External dependencies
|
||||||
|
*/
|
||||||
|
import { Command } from '@commander-js/extra-typings';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal dependencies
|
||||||
|
*/
|
||||||
|
import { replaceLabelsCommand } from './replace-labels';
|
||||||
|
|
||||||
|
const program = new Command( 'github' )
|
||||||
|
.description( 'Github utilities' )
|
||||||
|
.addCommand( replaceLabelsCommand );
|
||||||
|
|
||||||
|
export default program;
|
|
@ -0,0 +1,108 @@
|
||||||
|
/**
|
||||||
|
* External dependencies
|
||||||
|
*/
|
||||||
|
import { Command } from '@commander-js/extra-typings';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal dependencies
|
||||||
|
*/
|
||||||
|
import {
|
||||||
|
getIssuesByLabel,
|
||||||
|
updateIssue,
|
||||||
|
getRepositoryLabel,
|
||||||
|
} from '../../../core/github/repo';
|
||||||
|
import { Logger } from '../../../core/logger';
|
||||||
|
|
||||||
|
export const replaceLabelsCommand = new Command( 'replace-labels' )
|
||||||
|
.description( 'Replace labels of issues' )
|
||||||
|
.option(
|
||||||
|
'-o --owner <owner>',
|
||||||
|
'Repository owner. Default: woocommerce',
|
||||||
|
'woocommerce'
|
||||||
|
)
|
||||||
|
.option(
|
||||||
|
'-n --name <name>',
|
||||||
|
'Repository name. Default: woocommerce',
|
||||||
|
'woocommerce'
|
||||||
|
)
|
||||||
|
.option( '-l --label <label>', 'Label to filter by and replace' )
|
||||||
|
.option(
|
||||||
|
'-r --replacement-label <replacementLabel>',
|
||||||
|
'Label to use for replacement'
|
||||||
|
)
|
||||||
|
.option(
|
||||||
|
'--remove-if-starts-with <removeIfStartsWith>',
|
||||||
|
'Only remove the label if it already contains a label that starts with.'
|
||||||
|
)
|
||||||
|
.action( async ( options ) => {
|
||||||
|
const { owner, name, replacementLabel, removeIfStartsWith } = options;
|
||||||
|
const label = options.label?.toLowerCase();
|
||||||
|
|
||||||
|
if ( ! label ) {
|
||||||
|
Logger.warn(
|
||||||
|
`No label supplied, going off the latest release version`
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.startTask( `Querying by label: "${ label }"` );
|
||||||
|
const { results } = await getIssuesByLabel( { owner, name }, label );
|
||||||
|
Logger.endTask();
|
||||||
|
|
||||||
|
if ( results.length === 0 ) {
|
||||||
|
Logger.warn( `No issues found by label: "${ label }"` );
|
||||||
|
process.exit( 0 );
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
Logger.startTask(
|
||||||
|
`Checking if "${ replacementLabel }" exists in ${ name } repository.`
|
||||||
|
);
|
||||||
|
await getRepositoryLabel(
|
||||||
|
{ owner, name },
|
||||||
|
replacementLabel.toLowerCase()
|
||||||
|
);
|
||||||
|
Logger.endTask();
|
||||||
|
} catch ( e ) {
|
||||||
|
Logger.endTask();
|
||||||
|
Logger.warn(
|
||||||
|
`"${ replacementLabel }" does not exist in ${ name } repository. Please create the label first.`
|
||||||
|
);
|
||||||
|
process.exit( 0 );
|
||||||
|
}
|
||||||
|
|
||||||
|
for ( const issue of results ) {
|
||||||
|
// Get labels by name only and filter out the existing label.
|
||||||
|
const labels = issue.labels
|
||||||
|
.map( ( l ) => ( typeof l === 'string' ? l : l.name ) )
|
||||||
|
.filter( ( l ) => l.toLowerCase() !== label );
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if label with prefix already exists, in that case we only remove the label.
|
||||||
|
* Ex: Multiple teams may be assigned to one issue, when replace one team for another
|
||||||
|
* we did want to keep the team that already exists.
|
||||||
|
*/
|
||||||
|
const containsSimilarLabelAlready =
|
||||||
|
removeIfStartsWith &&
|
||||||
|
labels.find( ( l ) => l.startsWith( removeIfStartsWith ) );
|
||||||
|
|
||||||
|
if ( ! containsSimilarLabelAlready ) {
|
||||||
|
labels.push( replacementLabel );
|
||||||
|
}
|
||||||
|
Logger.notice(
|
||||||
|
`Updating issue ${ issue.number } labels to: ${ labels }`
|
||||||
|
);
|
||||||
|
const result = await updateIssue( { owner, name }, issue.number, {
|
||||||
|
labels,
|
||||||
|
} );
|
||||||
|
if ( result && result.status === 200 ) {
|
||||||
|
Logger.notice(
|
||||||
|
`Successfully updated issue ${ issue.number }: ${ result.data.html_url }`
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
Logger.error( `Failed updating ${ issue.number }` );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
process.exit( 0 );
|
||||||
|
} );
|
|
@ -10,6 +10,7 @@ import dotenv from 'dotenv';
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
*/
|
*/
|
||||||
import CodeFreeze from './code-freeze/commands';
|
import CodeFreeze from './code-freeze/commands';
|
||||||
|
import Github from './github/commands';
|
||||||
import Slack from './slack/commands/slack';
|
import Slack from './slack/commands/slack';
|
||||||
import Manifest from './md-docs/commands';
|
import Manifest from './md-docs/commands';
|
||||||
import Changefile from './changefile';
|
import Changefile from './changefile';
|
||||||
|
@ -38,7 +39,8 @@ const program = new Command()
|
||||||
.addCommand( CIJobs )
|
.addCommand( CIJobs )
|
||||||
.addCommand( WorkflowProfiler )
|
.addCommand( WorkflowProfiler )
|
||||||
.addCommand( Manifest )
|
.addCommand( Manifest )
|
||||||
.addCommand( SlackTestReport );
|
.addCommand( SlackTestReport )
|
||||||
|
.addCommand( Github );
|
||||||
|
|
||||||
program.exitOverride();
|
program.exitOverride();
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue