Remove CLI cherry pick tool in favor of CI workflow (#34954)
This commit is contained in:
parent
9f8ae8b46f
commit
3c8ca2887e
|
@ -1,13 +0,0 @@
|
|||
# Cherry Pick
|
||||
|
||||
A tool to automate cherry picking fixes into a release.
|
||||
|
||||
### Prerequisite
|
||||
|
||||
You will need to set two environment variable:
|
||||
* **GITHUB_CHERRY_PICK_TOKEN** - Generate a personal access token from GitHub and make sure it has the `Repo` scope. Assign this token to the environment variable.
|
||||
* **GITHUB_REMOTE_URL** - Depending on if you use `https` or `ssh` when you use `git clone`. Set the value `https` or `ssh` to that environment variable.
|
||||
|
||||
### Usage
|
||||
|
||||
Usage: `pnpm cherry-pick <release_branch> <pull_request_number>`. Separate pull request numbers with a comma (no space) if more than one.
|
|
@ -1,659 +0,0 @@
|
|||
/**
|
||||
* Tool to automate steps to cherry pick fixes into release branch.
|
||||
*
|
||||
* @package
|
||||
*/
|
||||
|
||||
import fetch from 'node-fetch';
|
||||
import os from 'os';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { spawnSync, spawn } from 'node:child_process';
|
||||
import ora from 'ora';
|
||||
|
||||
const args = process.argv.slice( 2 );
|
||||
const usage =
|
||||
'Usage: pnpm cherry-pick <release_branch> <pull_request_number>. Separate pull request numbers with a comma (no space) if more than one.';
|
||||
const releaseBranch = args[ 0 ];
|
||||
const prs = args[ 1 ];
|
||||
let githubToken = '';
|
||||
let tempWooDir = '';
|
||||
const changelogsToBeDeleted = [];
|
||||
const prCommits = {};
|
||||
let githubRemoteURL = 'https';
|
||||
|
||||
if ( typeof process.env.GITHUB_CHERRY_PICK_TOKEN === 'undefined' ) {
|
||||
console.error(
|
||||
'A GitHub token needs to be assigned to the "GITHUB_CHERRY_PICK_TOKEN" environment variable'
|
||||
);
|
||||
process.exit( 1 );
|
||||
}
|
||||
|
||||
if ( typeof process.env.GITHUB_REMOTE_URL !== 'undefined' ) {
|
||||
githubRemoteURL = process.env.GITHUB_REMOTE_URL;
|
||||
}
|
||||
|
||||
// If no arguments are passed.
|
||||
if ( ! args.length ) {
|
||||
console.error( usage );
|
||||
process.exit( 1 );
|
||||
}
|
||||
|
||||
// If missing one of the arguments.
|
||||
if ( typeof releaseBranch === 'undefined' || typeof prs === 'undefined' ) {
|
||||
console.error( usage );
|
||||
process.exit( 1 );
|
||||
}
|
||||
|
||||
// Accounts for multiple PRs.
|
||||
const prsArr = prs.split( ',' );
|
||||
const version = releaseBranch.replace( 'release/', '' );
|
||||
const cherryPickBranch =
|
||||
'cherry-pick-' + version + '/' + prsArr.toString().replace( ',', '-' );
|
||||
githubToken = process.env.GITHUB_CHERRY_PICK_TOKEN;
|
||||
|
||||
async function getCommitFromPrs( prsArr ) {
|
||||
const properties = {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Accept: 'application/vnd.github.v3+json',
|
||||
Authorization: `token ${ githubToken }`,
|
||||
},
|
||||
};
|
||||
|
||||
for ( const pr of prsArr ) {
|
||||
const response = await fetch(
|
||||
'https://api.github.com/repos/woocommerce/woocommerce/pulls/' + pr,
|
||||
properties
|
||||
);
|
||||
|
||||
if ( response.status !== 200 ) {
|
||||
throw 'One or more of the PR reference was not found.';
|
||||
}
|
||||
|
||||
await response.json().then( ( data ) => {
|
||||
prCommits[ pr ] = data.merge_commit_sha;
|
||||
} );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks out a branch.
|
||||
*
|
||||
* @param string branch The branch to checkout.
|
||||
* @param boolean newBranch A flag to create a new branch.
|
||||
* @param branch
|
||||
* @param newBranch
|
||||
*/
|
||||
function checkoutBranch( branch, newBranch = false ) {
|
||||
const spinner = ora( {
|
||||
spinner: 'bouncingBar',
|
||||
color: 'green',
|
||||
} );
|
||||
|
||||
const output = [];
|
||||
|
||||
return new Promise( ( resolve, reject ) => {
|
||||
const response = newBranch
|
||||
? spawn( 'git', [ 'checkout', '-b', branch ] )
|
||||
: spawn( 'git', [ 'checkout', branch ] );
|
||||
|
||||
response.stdout.on( 'data', ( data ) => {
|
||||
output.push( `${ data }` );
|
||||
} );
|
||||
|
||||
response.stderr.on( 'data', ( data ) => {
|
||||
output.push( `${ data }` );
|
||||
} );
|
||||
|
||||
response.on( 'close', ( code ) => {
|
||||
if ( `${ code }` == 0 ) {
|
||||
spinner.succeed( `Switched to '${ branch }'` );
|
||||
resolve();
|
||||
}
|
||||
|
||||
reject( `error: ${ output.toString().replace( ',', '\n' ) }` );
|
||||
} );
|
||||
} ).catch( ( err ) => {
|
||||
spinner.fail( 'Failed to switch branch' );
|
||||
throw err;
|
||||
} );
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the temp directory in system temp.
|
||||
*
|
||||
* @param string path The path to create.
|
||||
* @param path
|
||||
*/
|
||||
function createDir( path ) {
|
||||
return new Promise( ( resolve, reject ) => {
|
||||
fs.mkdtemp( path, ( err, directory ) => {
|
||||
if ( err ) {
|
||||
reject( err );
|
||||
}
|
||||
|
||||
tempWooDir = directory;
|
||||
|
||||
resolve();
|
||||
} );
|
||||
} ).catch( ( err ) => {
|
||||
throw err;
|
||||
} );
|
||||
}
|
||||
|
||||
/**
|
||||
* Clones the WooCommerce repo into temp dir.
|
||||
*
|
||||
* @param string woodir The temporary system directory.
|
||||
*/
|
||||
function cloneWoo() {
|
||||
const spinner = ora( {
|
||||
text: `Cloning WooCommerce into ${ tempWooDir }/woocommerce`,
|
||||
spinner: 'bouncingBar',
|
||||
color: 'green',
|
||||
} );
|
||||
|
||||
spinner.start();
|
||||
|
||||
const output = [];
|
||||
|
||||
return new Promise( ( resolve, reject ) => {
|
||||
let url = 'https://github.com/woocommerce/woocommerce.git';
|
||||
|
||||
if ( githubRemoteURL === 'ssh' ) {
|
||||
url = 'git@github.com:woocommerce/woocommerce.git';
|
||||
}
|
||||
|
||||
const response = spawn( 'git', [ 'clone', url ] );
|
||||
|
||||
response.stdout.on( 'data', ( data ) => {
|
||||
output.push( `${ data }` );
|
||||
} );
|
||||
|
||||
response.stderr.on( 'data', ( data ) => {
|
||||
output.push( `${ data }` );
|
||||
} );
|
||||
|
||||
response.on( 'close', ( code ) => {
|
||||
if ( `${ code }` == 0 ) {
|
||||
spinner.succeed();
|
||||
resolve();
|
||||
}
|
||||
|
||||
reject( `error: ${ output.toString().replace( ',', '\n' ) }` );
|
||||
} );
|
||||
} ).catch( ( err ) => {
|
||||
spinner.fail( 'Fail to clone WooCommerce!' );
|
||||
throw err;
|
||||
} );
|
||||
}
|
||||
|
||||
/**
|
||||
* Cherry picks.
|
||||
*
|
||||
* @param string commit The commit hash to cherry pick.
|
||||
* @param commit
|
||||
*/
|
||||
function cherryPick( commit ) {
|
||||
const spinner = ora( {
|
||||
text: `Cherry picking ${ commit }`,
|
||||
spinner: 'bouncingBar',
|
||||
color: 'green',
|
||||
} );
|
||||
|
||||
spinner.start();
|
||||
|
||||
const response = spawnSync( 'git', [ 'cherry-pick', commit ], {
|
||||
encoding: 'utf-8',
|
||||
} );
|
||||
|
||||
if ( response.status === 0 ) {
|
||||
spinner.succeed();
|
||||
return;
|
||||
}
|
||||
|
||||
if ( response.stderr ) {
|
||||
if (
|
||||
response.stderr.match( 'fatal: bad revision' ) ||
|
||||
response.stderr.match( 'error: could not apply' )
|
||||
) {
|
||||
spinner.fail( `Fail cherry picking ${ commit }` );
|
||||
console.log( response.stdout );
|
||||
throw `stderr: ${ response.stderr }`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Function to change directories.
|
||||
*
|
||||
* @param string dir The directory to change to.
|
||||
* @param dir
|
||||
*/
|
||||
function chdir( dir ) {
|
||||
try {
|
||||
process.chdir( dir );
|
||||
} catch ( e ) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the changelog for readme.txt.
|
||||
*
|
||||
* @param string pr The PR to use.
|
||||
* @param string commit The commit to use.
|
||||
* @param pr
|
||||
* @param commit
|
||||
*/
|
||||
async function generateChangelog( pr, commit ) {
|
||||
const properties = {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/vnd.github.v3+json',
|
||||
},
|
||||
};
|
||||
|
||||
if ( githubToken ) {
|
||||
properties.headers.Authorization = 'token ' + githubToken;
|
||||
}
|
||||
|
||||
const response = await fetch(
|
||||
'https://api.github.com/repos/woocommerce/woocommerce/commits/' +
|
||||
commit,
|
||||
properties
|
||||
);
|
||||
|
||||
if ( response.status !== 200 ) {
|
||||
throw 'Commit was not found.';
|
||||
}
|
||||
|
||||
let changelogTxt = '';
|
||||
|
||||
await response.json().then( ( data ) => {
|
||||
for ( const file of data.files ) {
|
||||
if ( file.filename.match( 'plugins/woocommerce/changelog/' ) ) {
|
||||
if ( changelogsToBeDeleted.indexOf( file.filename ) === -1 ) {
|
||||
changelogsToBeDeleted.push( file.filename );
|
||||
}
|
||||
|
||||
let changelogEntry = '';
|
||||
let changelogEntryType = '';
|
||||
|
||||
fs.readFile(
|
||||
'./' + file.filename,
|
||||
'utf-8',
|
||||
function ( err, data ) {
|
||||
if ( err ) {
|
||||
throw err;
|
||||
}
|
||||
|
||||
const changelogEntryArr = data.split( '\n' );
|
||||
changelogEntryType = data.match( /Type: (.+)/i );
|
||||
changelogEntryType =
|
||||
changelogEntryType[ 1 ].charAt( 0 ).toUpperCase() +
|
||||
changelogEntryType[ 1 ].slice( 1 );
|
||||
|
||||
changelogEntry = changelogEntryArr.filter( ( el ) => {
|
||||
return (
|
||||
el !== null &&
|
||||
typeof el !== 'undefined' &&
|
||||
el !== ''
|
||||
);
|
||||
} );
|
||||
changelogEntry =
|
||||
changelogEntry[ changelogEntry.length - 1 ];
|
||||
|
||||
// Check if changelogEntry is what we want.
|
||||
if ( changelogEntry.length < 1 ) {
|
||||
changelogEntry = false;
|
||||
}
|
||||
|
||||
if ( changelogEntry.match( /significance:/i ) ) {
|
||||
changelogEntry = false;
|
||||
}
|
||||
|
||||
if ( changelogEntry.match( /type:/i ) ) {
|
||||
changelogEntry = false;
|
||||
}
|
||||
|
||||
if ( changelogEntry.match( /comment:/i ) ) {
|
||||
changelogEntry = false;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
if ( changelogEntry === false ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
fs.readFile(
|
||||
'./plugins/woocommerce/readme.txt',
|
||||
'utf-8',
|
||||
function ( err, data ) {
|
||||
if ( err ) {
|
||||
throw err;
|
||||
}
|
||||
|
||||
const spinner = ora( {
|
||||
text: `Generating changelog entry for PR ${ pr }`,
|
||||
spinner: 'bouncingBar',
|
||||
color: 'green',
|
||||
} );
|
||||
|
||||
spinner.start();
|
||||
|
||||
changelogTxt = data.split( '\n' );
|
||||
let isInRange = false;
|
||||
const newChangelogTxt = [];
|
||||
|
||||
for ( const line of changelogTxt ) {
|
||||
if (
|
||||
isInRange === false &&
|
||||
line === '== Changelog =='
|
||||
) {
|
||||
isInRange = true;
|
||||
}
|
||||
|
||||
if (
|
||||
isInRange === true &&
|
||||
line.match( /\*\*WooCommerce Blocks/ )
|
||||
) {
|
||||
isInRange = false;
|
||||
}
|
||||
|
||||
// Find the first match of the entry "Type".
|
||||
if (
|
||||
isInRange &&
|
||||
line.match( `\\* ${ changelogEntryType } -` )
|
||||
) {
|
||||
newChangelogTxt.push(
|
||||
'* ' +
|
||||
changelogEntryType +
|
||||
' - ' +
|
||||
changelogEntry +
|
||||
` [#${ pr }](https://github.com/woocommerce/woocommerce/pull/${ pr })`
|
||||
);
|
||||
newChangelogTxt.push( line );
|
||||
isInRange = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
newChangelogTxt.push( line );
|
||||
}
|
||||
|
||||
fs.writeFile(
|
||||
'./plugins/woocommerce/readme.txt',
|
||||
newChangelogTxt.join( '\n' ),
|
||||
( err ) => {
|
||||
if ( err ) {
|
||||
spinner.fail(
|
||||
`Unable to generate the changelog entry for PR ${ pr }`
|
||||
);
|
||||
throw err;
|
||||
}
|
||||
|
||||
spinner.succeed();
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
} );
|
||||
}
|
||||
|
||||
/**
|
||||
* Git remove changelog files.
|
||||
*
|
||||
*/
|
||||
function deleteChangelogFiles() {
|
||||
const spinner = ora( {
|
||||
spinner: 'bouncingBar',
|
||||
color: 'green',
|
||||
} );
|
||||
|
||||
const files = changelogsToBeDeleted.join( ' ' );
|
||||
const filesFormatted = '\n' + files.replace( ' ', '\n' );
|
||||
const output = [];
|
||||
|
||||
return new Promise( ( resolve, reject ) => {
|
||||
const response = spawn( 'git', [ 'rm', files ], { shell: true } );
|
||||
|
||||
response.stdout.on( 'data', ( data ) => {
|
||||
output.push( `${ data }` );
|
||||
} );
|
||||
|
||||
response.stderr.on( 'data', ( data ) => {
|
||||
output.push( `${ data }` );
|
||||
} );
|
||||
|
||||
response.on( 'close', ( code ) => {
|
||||
if ( `${ code }` == 0 ) {
|
||||
spinner.succeed(
|
||||
`Removed changelog files:${ filesFormatted }`
|
||||
);
|
||||
resolve();
|
||||
}
|
||||
|
||||
reject( `error: ${ output.toString().replace( ',', '\n' ) }` );
|
||||
} );
|
||||
} ).catch( ( err ) => {
|
||||
spinner.fail( 'Fail to delete changelog files' );
|
||||
throw err;
|
||||
} );
|
||||
}
|
||||
|
||||
/**
|
||||
* Commit changes.
|
||||
*
|
||||
* @param string msg The message for the commit.
|
||||
* @param msg
|
||||
*/
|
||||
function commitChanges( msg ) {
|
||||
const spinner = ora( {
|
||||
spinner: 'bouncingBar',
|
||||
color: 'green',
|
||||
} );
|
||||
|
||||
const output = [];
|
||||
|
||||
return new Promise( ( resolve, reject ) => {
|
||||
const response = spawn( 'git', [
|
||||
'commit',
|
||||
'--no-verify',
|
||||
'-am',
|
||||
msg,
|
||||
] );
|
||||
|
||||
response.stdout.on( 'data', ( data ) => {
|
||||
output.push( `${ data }` );
|
||||
} );
|
||||
|
||||
response.stderr.on( 'data', ( data ) => {
|
||||
output.push( `${ data }` );
|
||||
} );
|
||||
|
||||
response.on( 'close', ( code ) => {
|
||||
if ( `${ code }` == 0 ) {
|
||||
spinner.succeed( `Commited changes.` );
|
||||
resolve();
|
||||
}
|
||||
|
||||
reject( `error: ${ output.toString().replace( ',', '\n' ) }` );
|
||||
} );
|
||||
} ).catch( ( err ) => {
|
||||
spinner.fail( 'Fail to commit changes.' );
|
||||
throw err;
|
||||
} );
|
||||
}
|
||||
|
||||
/**
|
||||
* Push the branch up to GitHub.
|
||||
*
|
||||
* @param string branch The branch to push to GitHub.
|
||||
* @param branch
|
||||
*/
|
||||
function pushBranch( branch ) {
|
||||
const spinner = ora( {
|
||||
spinner: 'bouncingBar',
|
||||
color: 'green',
|
||||
} );
|
||||
|
||||
spinner.start( `Pushing ${ branch } to GitHub...` );
|
||||
|
||||
const output = [];
|
||||
|
||||
return new Promise( ( resolve, reject ) => {
|
||||
const response = spawn( 'git', [ 'push', 'origin', branch ] );
|
||||
|
||||
response.stdout.on( 'data', ( data ) => {
|
||||
output.push( `${ data }` );
|
||||
} );
|
||||
|
||||
response.stderr.on( 'data', ( data ) => {
|
||||
output.push( `${ data }` );
|
||||
} );
|
||||
|
||||
response.on( 'close', ( code ) => {
|
||||
if ( `${ code }` == 0 ) {
|
||||
spinner.succeed();
|
||||
resolve();
|
||||
}
|
||||
|
||||
reject( `error: ${ output.toString().replace( ',', '\n' ) }` );
|
||||
} );
|
||||
} ).catch( ( err ) => {
|
||||
spinner.fail( `Fail to push ${ branch } up.` );
|
||||
throw err;
|
||||
} );
|
||||
}
|
||||
|
||||
/**
|
||||
* Create pull request on GitHub.
|
||||
*
|
||||
* @param string title The title of the PR.
|
||||
* @param string body The body content of the PR.
|
||||
* @param string head The head of the branch to use.
|
||||
* @param string base The base branch to targe against.
|
||||
* @param title
|
||||
* @param body
|
||||
* @param head
|
||||
* @param base
|
||||
*/
|
||||
async function createPR( title, body, head, base ) {
|
||||
const spinner = ora( {
|
||||
spinner: 'bouncingBar',
|
||||
color: 'green',
|
||||
} );
|
||||
|
||||
spinner.start( `Creating pull request for ${ head } on GitHub...` );
|
||||
|
||||
const response = await fetch(
|
||||
'https://api.github.com/repos/woocommerce/woocommerce/pulls',
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/vnd.github.v3+json',
|
||||
Authorization: `token ${ githubToken }`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify( {
|
||||
title,
|
||||
body,
|
||||
head,
|
||||
base,
|
||||
} ),
|
||||
}
|
||||
);
|
||||
|
||||
if ( response.status !== 201 ) {
|
||||
throw 'Fail to create PR on GitHub.';
|
||||
}
|
||||
|
||||
return await response.json().then( ( data ) => {
|
||||
spinner.succeed();
|
||||
return {
|
||||
url: data.url,
|
||||
number: data.number,
|
||||
};
|
||||
} );
|
||||
}
|
||||
|
||||
( async function () {
|
||||
try {
|
||||
// Creates a temp directory to work with.
|
||||
await createDir( path.join( os.tmpdir(), 'woo-cherry-pick-' ) );
|
||||
|
||||
// Gets the commits from GitHub based on the PRs passed in.
|
||||
await getCommitFromPrs( prsArr );
|
||||
|
||||
chdir( tempWooDir );
|
||||
|
||||
await cloneWoo();
|
||||
|
||||
chdir( tempWooDir + '/woocommerce' );
|
||||
|
||||
// This checks out the release branch.
|
||||
await checkoutBranch( releaseBranch );
|
||||
|
||||
// This checks out a new branch based on the release branch.
|
||||
await checkoutBranch( cherryPickBranch, true );
|
||||
|
||||
let cherryPickPRBody =
|
||||
'This PR cherry-picks the following PRs into the release branch:\n';
|
||||
|
||||
for ( const pr of Object.keys( prCommits ) ) {
|
||||
cherryPickPRBody = `${ cherryPickPRBody }` + `* #${ pr }` + '\n';
|
||||
cherryPick( prCommits[ pr ] );
|
||||
await generateChangelog( pr, prCommits[ pr ] );
|
||||
}
|
||||
|
||||
// Deletes the changelog files from the release branch.
|
||||
await deleteChangelogFiles();
|
||||
|
||||
await commitChanges( `Prep for cherry pick ${ prsArr.toString() }` );
|
||||
|
||||
await pushBranch( cherryPickBranch );
|
||||
|
||||
const cherryPickPR = await createPR(
|
||||
`Prep for cherry pick ${ prsArr.toString() }`,
|
||||
cherryPickPRBody,
|
||||
cherryPickBranch,
|
||||
releaseBranch
|
||||
);
|
||||
|
||||
await checkoutBranch( 'trunk' );
|
||||
|
||||
const deleteChangelogBranch = `delete-changelogs/${ prsArr
|
||||
.toString()
|
||||
.replace( ',', '-' ) }`;
|
||||
|
||||
// This checks out a new branch based on the trunk branch.
|
||||
await checkoutBranch( `${ deleteChangelogBranch }`, true );
|
||||
|
||||
// Deletes the changelog files from the trunk branch.
|
||||
await deleteChangelogFiles();
|
||||
|
||||
await commitChanges(
|
||||
`Delete changelog files for ${ prsArr.toString() }`
|
||||
);
|
||||
|
||||
await pushBranch( `${ deleteChangelogBranch }` );
|
||||
|
||||
const deleteChangelogsPR = await createPR(
|
||||
`Delete changelog files based on PR ${ cherryPickPR.number }`,
|
||||
`Delete changelog files based on PR #${ cherryPickPR.number }`,
|
||||
deleteChangelogBranch,
|
||||
'trunk'
|
||||
);
|
||||
|
||||
console.log( `Two PRs created by this process:` );
|
||||
console.log( cherryPickPR.url );
|
||||
console.log( deleteChangelogsPR.url );
|
||||
} catch ( e ) {
|
||||
console.error( e );
|
||||
process.exit( 1 );
|
||||
}
|
||||
} )();
|
|
@ -1,21 +0,0 @@
|
|||
{
|
||||
"name": "cherry-pick",
|
||||
"version": "0.0.0",
|
||||
"description": "A tool to automate cherry picking fixes into a release.",
|
||||
"author": "Automattic",
|
||||
"scripts": {
|
||||
"cherry-pick": "./bin/run"
|
||||
},
|
||||
"type": "module",
|
||||
"homepage": "https://github.com/woocommerce/woocommerce",
|
||||
"license": "GPLv3",
|
||||
"repository": "woocommerce/woocommerce",
|
||||
"engines": {
|
||||
"node": "^16.13.1",
|
||||
"pnpm": "^7.12.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"node-fetch": "^3.2.6",
|
||||
"ora": "^6.1.2"
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue