Cherry-Pick Tool: Fix stderr output (#34132)

* formatting

* log stdout
This commit is contained in:
Paul Sealock 2022-08-03 06:54:41 +12:00 committed by GitHub
parent a9b0542532
commit 0ac969f584
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
1 changed files with 286 additions and 203 deletions

View File

@ -11,18 +11,21 @@ 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 = '';
let changelogsToBeDeleted = [];
let prCommits = {};
let githubRemoteURL = 'https';
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' );
console.error(
'A GitHub token needs to be assigned to the "GITHUB_CHERRY_PICK_TOKEN" environment variable'
);
process.exit( 1 );
}
@ -43,28 +46,32 @@ if ( typeof releaseBranch === 'undefined' || typeof prs === 'undefined' ) {
}
// 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;
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 ) {
let properties = {
method: 'GET',
headers: {
'Accept': 'application/vnd.github.v3+json',
'Authorization': `token ${githubToken}`
}
};
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 );
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 => {
await response.json().then( ( data ) => {
prCommits[ pr ] = data.merge_commit_sha;
} );
}
@ -73,37 +80,41 @@ async function getCommitFromPrs( prsArr ) {
/**
* Checks out a branch.
*
* @param string branch The branch to checkout.
* @param boolean newBranch A flag to create a new 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'
color: 'green',
} );
let output = [];
const output = [];
return new Promise( ( resolve, reject ) => {
const response = newBranch ? spawn( 'git', [ 'checkout', '-b', branch ] ) : spawn( 'git', [ 'checkout', branch ] );
const response = newBranch
? spawn( 'git', [ 'checkout', '-b', branch ] )
: spawn( 'git', [ 'checkout', branch ] );
response.stdout.on( 'data', ( data ) => {
output.push( `${data}` );
output.push( `${ data }` );
} );
response.stderr.on( 'data', ( data ) => {
output.push( `${data}` );
output.push( `${ data }` );
} );
response.on( 'close', ( code ) => {
if ( `${code}` == 0 ) {
spinner.succeed( `Switched to '${branch}'` );
if ( `${ code }` == 0 ) {
spinner.succeed( `Switched to '${ branch }'` );
resolve();
}
reject( `error: ${output.toString().replace( ',', "\n" )}` );
reject( `error: ${ output.toString().replace( ',', '\n' ) }` );
} );
} ).catch( err => {
} ).catch( ( err ) => {
spinner.fail( 'Failed to switch branch' );
throw err;
} );
@ -112,7 +123,8 @@ function checkoutBranch( branch, newBranch = false ) {
/**
* Create the temp directory in system temp.
*
* @param string path The path to create.
* @param string path The path to create.
* @param path
*/
function createDir( path ) {
return new Promise( ( resolve, reject ) => {
@ -125,7 +137,7 @@ function createDir( path ) {
resolve();
} );
} ).catch( err => {
} ).catch( ( err ) => {
throw err;
} );
}
@ -133,18 +145,18 @@ function createDir( path ) {
/**
* Clones the WooCommerce repo into temp dir.
*
* @param string woodir The temporary system directory.
* @param string woodir The temporary system directory.
*/
function cloneWoo() {
const spinner = ora( {
text: `Cloning WooCommerce into ${tempWooDir}/woocommerce`,
text: `Cloning WooCommerce into ${ tempWooDir }/woocommerce`,
spinner: 'bouncingBar',
color: 'green'
color: 'green',
} );
spinner.start();
let output = [];
const output = [];
return new Promise( ( resolve, reject ) => {
let url = 'https://github.com/woocommerce/woocommerce.git';
@ -156,22 +168,22 @@ function cloneWoo() {
const response = spawn( 'git', [ 'clone', url ] );
response.stdout.on( 'data', ( data ) => {
output.push( `${data}` );
output.push( `${ data }` );
} );
response.stderr.on( 'data', ( data ) => {
output.push( `${data}` );
output.push( `${ data }` );
} );
response.on( 'close', ( code ) => {
if ( `${code}` == 0 ) {
if ( `${ code }` == 0 ) {
spinner.succeed();
resolve();
}
reject( `error: ${output.toString().replace( ',', "\n" )}` );
reject( `error: ${ output.toString().replace( ',', '\n' ) }` );
} );
} ).catch( err => {
} ).catch( ( err ) => {
spinner.fail( 'Fail to clone WooCommerce!' );
throw err;
} );
@ -180,36 +192,44 @@ function cloneWoo() {
/**
* Cherry picks.
*
* @param string commit The commit hash to cherry pick.
* @param string commit The commit hash to cherry pick.
* @param commit
*/
function cherryPick( commit ) {
const spinner = ora( {
text: `Cherry picking ${commit}`,
text: `Cherry picking ${ commit }`,
spinner: 'bouncingBar',
color: 'green'
color: 'green',
} );
spinner.start();
const response = spawnSync( 'git', [ 'cherry-pick', commit ] );
const response = spawnSync( 'git', [ 'cherry-pick', commit ], {
encoding: 'utf-8',
} );
if ( response.status == 0 ) {
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}` );
throw `stderr: ${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 string dir The directory to change to.
* @param dir
*/
function chdir( dir ) {
try {
@ -222,22 +242,28 @@ function chdir( dir ) {
/**
* Generates the changelog for readme.txt.
*
* @param string pr The PR to use.
* @param string commit The commit to use.
* @param string pr The PR to use.
* @param string commit The commit to use.
* @param pr
* @param commit
*/
async function generateChangelog( pr, commit ) {
let properties = {
method: 'GET',
headers: {
'Content-Type': 'application/vnd.github.v3+json'
}
};
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 );
const response = await fetch(
'https://api.github.com/repos/woocommerce/woocommerce/commits/' +
commit,
properties
);
if ( response.status !== 200 ) {
throw 'Commit was not found.';
@ -245,98 +271,134 @@ async function generateChangelog( pr, commit ) {
let changelogTxt = '';
await response.json().then( data => {
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 changelogEntry = '';
let changelogEntryType = '';
fs.readFile( './' + file.filename, 'utf-8', function( err, data ) {
if ( err ) {
throw err;
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;
}
}
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;
let 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 => {
fs.readFile(
'./plugins/woocommerce/readme.txt',
'utf-8',
function ( err, data ) {
if ( err ) {
spinner.fail( `Unable to generate the changelog entry for PR ${pr}` );
throw err;
}
spinner.succeed();
} );
} );
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();
}
);
}
);
}
}
} );
@ -349,33 +411,35 @@ async function generateChangelog( pr, commit ) {
function deleteChangelogFiles() {
const spinner = ora( {
spinner: 'bouncingBar',
color: 'green'
color: 'green',
} );
const files = changelogsToBeDeleted.join( ' ' );
const filesFormatted = "\n" + files.replace( ' ', "\n" );
let output = [];
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}` );
output.push( `${ data }` );
} );
response.stderr.on( 'data', ( data ) => {
output.push( `${data}` );
output.push( `${ data }` );
} );
response.on( 'close', ( code ) => {
if ( `${code}` == 0 ) {
spinner.succeed( `Removed changelog files:${filesFormatted}` );
if ( `${ code }` == 0 ) {
spinner.succeed(
`Removed changelog files:${ filesFormatted }`
);
resolve();
}
reject( `error: ${output.toString().replace( ',', "\n" )}` );
reject( `error: ${ output.toString().replace( ',', '\n' ) }` );
} );
} ).catch( err => {
} ).catch( ( err ) => {
spinner.fail( 'Fail to delete changelog files' );
throw err;
} );
@ -384,36 +448,42 @@ function deleteChangelogFiles() {
/**
* Commit changes.
*
* @param string msg The message for the commit.
* @param string msg The message for the commit.
* @param msg
*/
function commitChanges( msg ) {
const spinner = ora( {
spinner: 'bouncingBar',
color: 'green'
color: 'green',
} );
let output = [];
const output = [];
return new Promise( ( resolve, reject ) => {
const response = spawn( 'git', [ 'commit', '--no-verify', '-am', msg ] );
const response = spawn( 'git', [
'commit',
'--no-verify',
'-am',
msg,
] );
response.stdout.on( 'data', ( data ) => {
output.push( `${data}` );
output.push( `${ data }` );
} );
response.stderr.on( 'data', ( data ) => {
output.push( `${data}` );
output.push( `${ data }` );
} );
response.on( 'close', ( code ) => {
if ( `${code}` == 0 ) {
if ( `${ code }` == 0 ) {
spinner.succeed( `Commited changes.` );
resolve();
}
reject( `error: ${output.toString().replace( ',', "\n" )}` );
reject( `error: ${ output.toString().replace( ',', '\n' ) }` );
} );
} ).catch( err => {
} ).catch( ( err ) => {
spinner.fail( 'Fail to commit changes.' );
throw err;
} );
@ -422,39 +492,40 @@ function commitChanges( msg ) {
/**
* Push the branch up to GitHub.
*
* @param string branch The branch to push to GitHub.
* @param string branch The branch to push to GitHub.
* @param branch
*/
function pushBranch( branch ) {
const spinner = ora( {
spinner: 'bouncingBar',
color: 'green'
color: 'green',
} );
spinner.start( `Pushing ${branch} to GitHub...` );
spinner.start( `Pushing ${ branch } to GitHub...` );
let output = [];
const output = [];
return new Promise( ( resolve, reject ) => {
const response = spawn( 'git', [ 'push', 'origin', branch ] );
response.stdout.on( 'data', ( data ) => {
output.push( `${data}` );
output.push( `${ data }` );
} );
response.stderr.on( 'data', ( data ) => {
output.push( `${data}` );
output.push( `${ data }` );
} );
response.on( 'close', ( code ) => {
if ( `${code}` == 0 ) {
if ( `${ code }` == 0 ) {
spinner.succeed();
resolve();
}
reject( `error: ${output.toString().replace( ',', "\n" )}` );
reject( `error: ${ output.toString().replace( ',', '\n' ) }` );
} );
} ).catch( err => {
spinner.fail( `Fail to push ${branch} up.` );
} ).catch( ( err ) => {
spinner.fail( `Fail to push ${ branch } up.` );
throw err;
} );
}
@ -462,51 +533,58 @@ function pushBranch( branch ) {
/**
* 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 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'
color: 'green',
} );
spinner.start( `Creating pull request for ${head} on GitHub...` );
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": title,
"body": body,
"head": head,
"base": base
} )
} );
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 => {
return await response.json().then( ( data ) => {
spinner.succeed();
return {
"url": data.url,
"number": data.number
}
url: data.url,
number: data.number,
};
} );
}
( async function() {
( async function () {
try {
// Creates a temp directory to work with.
await createDir( path.join( os.tmpdir(), 'woo-cherry-pick-') );
await createDir( path.join( os.tmpdir(), 'woo-cherry-pick-' ) );
// Gets the commits from GitHub based on the PRs passed in.
await getCommitFromPrs( prsArr );
@ -523,23 +601,24 @@ async function createPR( title, body, head, base ) {
// 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";
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";
cherryPickPRBody = `${ cherryPickPRBody }` + `* #${ pr }` + '\n';
cherryPick( prCommits[ pr ] );
await generateChangelog( pr, 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 commitChanges( `Prep for cherry pick ${ prsArr.toString() }` );
await pushBranch( cherryPickBranch );
const cherryPickPR = await createPR(
`Prep for cherry pick ${prsArr.toString()}`,
`Prep for cherry pick ${ prsArr.toString() }`,
cherryPickPRBody,
cherryPickBranch,
releaseBranch
@ -547,21 +626,25 @@ async function createPR( title, body, head, base ) {
await checkoutBranch( 'trunk' );
const deleteChangelogBranch = `delete-changelogs/${prsArr.toString().replace( ',', '-' )}`;
const deleteChangelogBranch = `delete-changelogs/${ prsArr
.toString()
.replace( ',', '-' ) }`;
// This checks out a new branch based on the trunk branch.
await checkoutBranch( `${deleteChangelogBranch}`, true );
await checkoutBranch( `${ deleteChangelogBranch }`, true );
// Deletes the changelog files from the trunk branch.
await deleteChangelogFiles();
await commitChanges( `Delete changelog files for ${prsArr.toString()}` );
await commitChanges(
`Delete changelog files for ${ prsArr.toString() }`
);
await pushBranch( `${deleteChangelogBranch}` );
await pushBranch( `${ deleteChangelogBranch }` );
const deleteChangelogsPR = await createPR(
`Delete changelog files based on PR ${cherryPickPR.number}`,
`Delete changelog files based on PR #${cherryPickPR.number}`,
`Delete changelog files based on PR ${ cherryPickPR.number }`,
`Delete changelog files based on PR #${ cherryPickPR.number }`,
deleteChangelogBranch,
'trunk'
);