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:
louwie17 2024-09-03 15:27:50 +02:00 committed by GitHub
parent a7f29ce98e
commit f44c0b8064
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 225 additions and 2 deletions

File diff suppressed because one or more lines are too long

View File

@ -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;

View File

@ -0,0 +1,3 @@
# Github CLI Utility
CLI for performing Monorepo utilities relating to Github issues.

View File

@ -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;

View File

@ -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 );
} );

View File

@ -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();