Add cherry pick tool (#33870)

* Add cherry pick tool

* Add githubremoteurl environment

* Add readme
This commit is contained in:
Roy Ho 2022-07-14 07:36:15 -07:00 committed by GitHub
parent 87e1f3106d
commit 2774ef2c1b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 732 additions and 4 deletions

View File

@ -20,7 +20,8 @@
"git:update-hooks": "rm -r .git/hooks && mkdir -p .git/hooks && husky install",
"storybook": "./tools/storybook/import-wp-css-storybook.sh && BABEL_ENV=storybook STORYBOOK=true start-storybook -c ./tools/storybook/.storybook -p 6007 --ci",
"storybook-rtl": "USE_RTL_STYLE=true pnpm run storybook",
"create-extension": "node ./tools/create-extension/index.js"
"create-extension": "node ./tools/create-extension/index.js",
"cherry-pick": "node ./tools/cherry-pick/bin/run"
},
"devDependencies": {
"@babel/preset-env": "^7.16.11",

View File

@ -1737,6 +1737,14 @@ importers:
sass: 1.49.9
stylelint: 13.8.0
tools/cherry-pick:
specifiers:
node-fetch: ^3.2.6
ora: ^6.1.2
dependencies:
node-fetch: 3.2.8
ora: 6.1.2
tools/code-analyzer:
specifiers:
'@oclif/core': ^1
@ -16845,7 +16853,6 @@ packages:
/ansi-regex/6.0.1:
resolution: {integrity: sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==}
engines: {node: '>=12'}
dev: true
/ansi-styles/2.2.1:
resolution: {integrity: sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA==}
@ -18243,6 +18250,14 @@ packages:
readable-stream: 3.6.0
dev: true
/bl/5.0.0:
resolution: {integrity: sha512-8vxFNZ0pflFfi0WXA3WQXlj6CaMEwsmh63I1CNp0q+wWv8sD0ARx1KovSQd0l2GkwrMIOyedq0EF1FxI+RCZLQ==}
dependencies:
buffer: 6.0.3
inherits: 2.0.4
readable-stream: 3.6.0
dev: false
/blob/0.0.5:
resolution: {integrity: sha512-gaqbzQPqOoamawKg0LGVd7SzLgXS+JH61oWprSLH+P+abTczqJbhTR8CmJ2u9/bUYNmHTGJx/UEmn6doAvvuig==}
dev: false
@ -18545,6 +18560,13 @@ packages:
base64-js: 1.5.1
ieee754: 1.2.1
/buffer/6.0.3:
resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==}
dependencies:
base64-js: 1.5.1
ieee754: 1.2.1
dev: false
/builtin-status-codes/2.0.0:
resolution: {integrity: sha512-8KPx+JfZWi0K8L5sycIOA6/ZFZbaFKXDeUIXaqwUnhed1Ge1cB0wyq+bNDjKnL9AR2Uj3m/khkF6CDolsyMitA==}
dev: false
@ -18877,6 +18899,11 @@ packages:
ansi-styles: 4.3.0
supports-color: 7.2.0
/chalk/5.0.1:
resolution: {integrity: sha512-Fo07WOYGqMfCWHOzSXOt2CxDbC6skS/jO9ynEcmpANMoPrD+W1r1K6Vx7iNm+AQmETU1Xr2t+n8nzkV9t6xh3w==}
engines: {node: ^12.17.0 || ^14.13 || >=16.0.0}
dev: false
/change-case/2.3.1:
resolution: {integrity: sha1-LE/ePwY7tB0AzWjg1aCdthy+iU8=}
dependencies:
@ -19168,6 +19195,13 @@ packages:
dependencies:
restore-cursor: 3.1.0
/cli-cursor/4.0.0:
resolution: {integrity: sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
dependencies:
restore-cursor: 4.0.0
dev: false
/cli-progress/3.10.0:
resolution: {integrity: sha512-kLORQrhYCAtUPLZxqsAt2YJGOvRdt34+O6jl5cQGb7iF3dM55FQZlTR+rQyIK9JUcO9bBMwZsTlND+3dmFU2Cw==}
engines: {node: '>=4'}
@ -19177,7 +19211,6 @@ packages:
/cli-spinners/2.6.1:
resolution: {integrity: sha512-x/5fWmGMnbKQAaNwN+UZlV79qBLM9JFnJuJ03gIi5whrob0xV0ofNVHy9DhwGdsMJQc2OKv0oGmLzvaqvAVv+g==}
engines: {node: '>=6'}
dev: true
/cli-table/0.3.11:
resolution: {integrity: sha512-IqLQi4lO0nIB4tcdTpN4LCB9FI3uqrJZK7RC515EnhZ6qBaglkIgICb1wjeAqpdoOabm1+SuQtkXIPdYC93jhQ==}
@ -20341,6 +20374,11 @@ packages:
dependencies:
assert-plus: 1.0.0
/data-uri-to-buffer/4.0.0:
resolution: {integrity: sha512-Vr3mLBA8qWmcuschSLAOogKgQ/Jwxulv3RNE4FXnYWRGujzrRWQI4m12fQqRkwX06C0KanhLr4hK+GydchZsaA==}
engines: {node: '>= 12'}
dev: false
/data-urls/1.1.0:
resolution: {integrity: sha512-YTWYI9se1P55u58gL5GkQHW4P6VJBJ5iBT+B5a7i2Tjadhv52paJG0qHX4A0OR6/t52odI64KP2YvFpkDOi3eQ==}
dependencies:
@ -23174,6 +23212,14 @@ packages:
dependencies:
pend: 1.2.0
/fetch-blob/3.2.0:
resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==}
engines: {node: ^12.20 || >= 14.13}
dependencies:
node-domexception: 1.0.0
web-streams-polyfill: 3.2.1
dev: false
/figgy-pudding/3.5.2:
resolution: {integrity: sha512-0btnI/H8f2pavGMN8w40mlSKOfTK2SVJmBfBeVIj3kNw0swwgzyRq0d5TJVOwodFmtvpPeWPN/MCcfuWF0Ezbw==}
dev: true
@ -23742,6 +23788,13 @@ packages:
engines: {node: '>=0.4.x'}
dev: true
/formdata-polyfill/4.0.10:
resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==}
engines: {node: '>=12.20.0'}
dependencies:
fetch-blob: 3.2.0
dev: false
/formidable/1.2.2:
resolution: {integrity: sha512-V8gLm+41I/8kguQ4/o1D3RIHRmhYFG4pnNyonvua+40rqcEmT4+V71yaZ3B457xbbgCsCfjSPi65u/W6vK1U5Q==}
dev: false
@ -25907,6 +25960,11 @@ packages:
engines: {node: '>=8'}
dev: true
/is-interactive/2.0.0:
resolution: {integrity: sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==}
engines: {node: '>=12'}
dev: false
/is-lambda/1.0.1:
resolution: {integrity: sha1-PZh3iZ5qU+/AFgUEzeFfgubwYdU=}
dev: true
@ -26104,6 +26162,11 @@ packages:
engines: {node: '>=10'}
dev: true
/is-unicode-supported/1.2.0:
resolution: {integrity: sha512-wH+U77omcRzevfIG8dDhTS0V9zZyweakfD01FULl97+0EHiJTTZtJqxPSkIIo/SDPv/i07k/C9jAPY+jwLLeUQ==}
engines: {node: '>=12'}
dev: false
/is-upper-case/1.1.2:
resolution: {integrity: sha1-jQsfp+eTOh5YSDYA7H2WYcuvdW8=}
dependencies:
@ -29502,6 +29565,14 @@ packages:
is-unicode-supported: 0.1.0
dev: true
/log-symbols/5.1.0:
resolution: {integrity: sha512-l0x2DvrW294C9uDCoQe1VSU4gf529FkSZ6leBl4TiqZH/e+0R7hSfHQBNut2mNygDgHwvYHfFLn6Oxb3VWj2rA==}
engines: {node: '>=12'}
dependencies:
chalk: 5.0.1
is-unicode-supported: 1.2.0
dev: false
/log-update/4.0.0:
resolution: {integrity: sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg==}
engines: {node: '>=10'}
@ -30608,6 +30679,11 @@ packages:
minimatch: 3.1.2
dev: true
/node-domexception/1.0.0:
resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==}
engines: {node: '>=10.5.0'}
dev: false
/node-environment-flags/1.0.6:
resolution: {integrity: sha512-5Evy2epuL+6TM0lCQGpFIj6KwiEsGh1SrHUhTbNX+sLbBtjidPZFAnVK9y5yU1+h//RitLbRHTIMyxQPtxMdHw==}
dependencies:
@ -30644,6 +30720,15 @@ packages:
dependencies:
whatwg-url: 5.0.0
/node-fetch/3.2.8:
resolution: {integrity: sha512-KtpD1YhGszhntMpBDyp5lyagk8KIMopC1LEb7cQUAh7zcosaX5uK8HnbNb2i3NTQK3sIawCItS0uFC3QzcLHdg==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
dependencies:
data-uri-to-buffer: 4.0.0
fetch-blob: 3.2.0
formdata-polyfill: 4.0.10
dev: false
/node-gyp/8.4.1:
resolution: {integrity: sha512-olTJRgUtAb/hOXG0E93wZDs5YiJlgbXxTwQAFHyNlRsXQnYzUaF2aGgujZbw+hR8aF4ZG/rST57bWMWD16jr9w==}
engines: {node: '>= 10.12.0'}
@ -31261,6 +31346,21 @@ packages:
wcwidth: 1.0.1
dev: true
/ora/6.1.2:
resolution: {integrity: sha512-EJQ3NiP5Xo94wJXIzAyOtSb0QEIAUu7m8t6UZ9krbz0vAJqr92JpcK/lEXg91q6B9pEGqrykkd2EQplnifDSBw==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
dependencies:
bl: 5.0.0
chalk: 5.0.1
cli-cursor: 4.0.0
cli-spinners: 2.6.1
is-interactive: 2.0.0
is-unicode-supported: 1.2.0
log-symbols: 5.1.0
strip-ansi: 7.0.1
wcwidth: 1.0.1
dev: false
/os-browserify/0.3.0:
resolution: {integrity: sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc=}
dev: true
@ -34837,6 +34937,14 @@ packages:
onetime: 5.1.2
signal-exit: 3.0.7
/restore-cursor/4.0.0:
resolution: {integrity: sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
dependencies:
onetime: 5.1.2
signal-exit: 3.0.7
dev: false
/ret/0.1.15:
resolution: {integrity: sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==}
engines: {node: '>=0.12'}
@ -35966,7 +36074,6 @@ packages:
engines: {node: '>=12'}
dependencies:
ansi-regex: 6.0.1
dev: true
/strip-bom-buf/1.0.0:
resolution: {integrity: sha1-HLRar1dTD0yvhsf3UXnSyaUd1XI=}
@ -38543,6 +38650,11 @@ packages:
resolution: {integrity: sha512-wYxSGajtmoP4WxfejAPIr4l0fVh+jeMXZb08wNc0tMg6xsfZXj3cECqIK0G7ZAqUq0PP8WlMDtaOGVBTAWztNw==}
dev: true
/web-streams-polyfill/3.2.1:
resolution: {integrity: sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q==}
engines: {node: '>= 8'}
dev: false
/webidl-conversions/3.0.1:
resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}

View File

@ -6,4 +6,5 @@ packages:
- 'tools/code-analyzer'
- 'tools/create-extension'
- 'tools/package-release'
- 'tools/cherry-pick'
- 'tools/require-turbo'

View File

@ -0,0 +1,13 @@
# 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.

View File

@ -0,0 +1,580 @@
/**
* 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 = '';
let changelogsToBeDeleted = [];
let 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 ) {
let 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.
*/
function checkoutBranch( branch, newBranch = false ) {
const spinner = ora( {
spinner: 'bouncingBar',
color: 'green'
} );
let output = [];
return new Promise( ( resolve, reject ) => {
let response = spawn( 'git', [ 'checkout', branch ] );
if ( newBranch ) {
response = spawn( 'git', [ 'checkout', '-b', 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.
*/
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();
let 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.
*/
function cherryPick( commit ) {
const spinner = ora( {
text: `Cherry picking ${commit}`,
spinner: 'bouncingBar',
color: 'green'
} );
spinner.start();
const response = spawnSync( 'git', [ 'cherry-pick', commit ] );
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}`;
}
};
}
/**
* Function to change directories.
*
* @param string dir The directory to change to.
*/
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.
*/
async function generateChangelog( pr, commit ) {
let 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;
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 => {
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" );
let 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.
*/
function commitChanges( msg ) {
const spinner = ora( {
spinner: 'bouncingBar',
color: 'green'
} );
let 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.
*/
function pushBranch( branch ) {
const spinner = ora( {
spinner: 'bouncingBar',
color: 'green'
} );
spinner.start( `Pushing ${branch} to GitHub...` );
let 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.
*/
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": title,
"body": body,
"head": head,
"base": 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 );
}
} )();

View File

@ -0,0 +1,21 @@
{
"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": "^6.24.2"
},
"dependencies": {
"node-fetch": "^3.2.6",
"ora": "^6.1.2"
}
}