Add cherry pick tool (#33870)
* Add cherry pick tool * Add githubremoteurl environment * Add readme
This commit is contained in:
parent
87e1f3106d
commit
2774ef2c1b
|
@ -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",
|
||||
|
|
118
pnpm-lock.yaml
118
pnpm-lock.yaml
|
@ -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==}
|
||||
|
||||
|
|
|
@ -6,4 +6,5 @@ packages:
|
|||
- 'tools/code-analyzer'
|
||||
- 'tools/create-extension'
|
||||
- 'tools/package-release'
|
||||
- 'tools/cherry-pick'
|
||||
- 'tools/require-turbo'
|
||||
|
|
|
@ -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.
|
|
@ -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 );
|
||||
}
|
||||
} )();
|
|
@ -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"
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue