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
|
||||
*/
|
||||
import { Repository } from '@octokit/graphql-schema';
|
||||
import { Endpoints } from '@octokit/types';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
|
@ -39,6 +40,100 @@ export const getLatestGithubReleaseVersion = async ( options: {
|
|||
).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 (
|
||||
options: {
|
||||
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
|
||||
*/
|
||||
import CodeFreeze from './code-freeze/commands';
|
||||
import Github from './github/commands';
|
||||
import Slack from './slack/commands/slack';
|
||||
import Manifest from './md-docs/commands';
|
||||
import Changefile from './changefile';
|
||||
|
@ -38,7 +39,8 @@ const program = new Command()
|
|||
.addCommand( CIJobs )
|
||||
.addCommand( WorkflowProfiler )
|
||||
.addCommand( Manifest )
|
||||
.addCommand( SlackTestReport );
|
||||
.addCommand( SlackTestReport )
|
||||
.addCommand( Github );
|
||||
|
||||
program.exitOverride();
|
||||
|
||||
|
|
Loading…
Reference in New Issue