CLI tool and GH workflow to highlight PR changes (#32063)

This commit is contained in:
Claudio Sanches 2022-05-16 22:47:17 -03:00 committed by GitHub
parent 02ef6c534e
commit 2f4f8277bf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 864 additions and 49 deletions

View File

@ -0,0 +1,88 @@
name: Highlight templates and hooks changes
on: pull_request
jobs:
analyze:
name: Check pull request changes to highlight
runs-on: ubuntu-20.04
outputs:
results: ${{ steps.results.outputs.results }}
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Install prerequisites
run: |
npm install -g pnpm@^6.24.2
pnpm install
- name: Run analyzer
id: run
run: ./tools/code-analyzer/bin/dev analyzer "$GITHUB_HEAD_REF" -o github
- name: Print results
id: results
run: echo "::set-output name=results::${{ steps.run.outputs.templates }}${{ steps.run.outputs.wphooks }}"
comment:
name: Add comment to hightlight changes
needs: analyze
runs-on: ubuntu-20.04
steps:
- name: Find Comment
uses: peter-evans/find-comment@v2
id: find-comment
with:
issue-number: ${{ github.event.number }}
comment-author: woocommercebot
- name: Add comment
if: ${{ needs.analyze.outputs.results && (steps.find-comment.outputs.comment-id == '') }}
uses: actions/github-script@v5
with:
github-token: ${{ secrets.WC_BOT_TRIAGE_TOKEN }}
script: |
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: '## New hook or template changes in this PR${{ needs.analyze.outputs.results }}'
})
- name: Update comment
if: ${{ needs.analyze.outputs.results && steps.find-comment.outputs.comment-id }}
uses: actions/github-script@v5
with:
github-token: ${{ secrets.WC_BOT_TRIAGE_TOKEN }}
script: |
github.rest.issues.updateComment({
comment_id: ${{ steps.find-comment.outputs.comment-id }},
owner: context.repo.owner,
repo: context.repo.repo,
body: '## New hook or template changes in this PR${{ needs.analyze.outputs.results }}'
})
- name: Delete comment
if: ${{ !needs.analyze.outputs.results && steps.find-comment.outputs.comment-id }}
uses: izhangzhihao/delete-comment@master
with:
github_token: ${{ secrets.WC_BOT_TRIAGE_TOKEN }}
delete_user_name: woocommercebot
issue_number: ${{ github.event.number }}
- name: Add label
if: ${{ needs.analyze.outputs.results }}
uses: actions/github-script@v5
with:
github-token: ${{ secrets.WC_BOT_TRIAGE_TOKEN }}
script: |
github.rest.issues.addLabels({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
labels: ['release: highlight']
})
- name: Remove label
if: ${{ !needs.analyze.outputs.results }}
continue-on-error: true
uses: actions/github-script@v5
with:
github-token: ${{ secrets.WC_BOT_TRIAGE_TOKEN }}
script: |
github.rest.issues.removeLabel({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
name: ['release: highlight']
})

View File

@ -1643,6 +1643,33 @@ importers:
sass: 1.45.0
stylelint: 13.8.0
tools/code-analyzer:
specifiers:
'@oclif/core': ^1
'@oclif/plugin-help': ^5
'@oclif/plugin-plugins': ^2.0.1
'@types/node': ^16.9.4
eslint: ^7.32.0
globby: ^11
oclif: ^2
shx: ^0.3.3
ts-node: ^10.2.1
tslib: ^2.3.1
typescript: ^4.4.3
dependencies:
'@oclif/core': 1.3.4
'@oclif/plugin-help': 5.1.11
'@oclif/plugin-plugins': 2.1.0
devDependencies:
'@types/node': 16.10.3
eslint: 7.32.0
globby: 11.1.0
oclif: 2.4.5
shx: 0.3.4
ts-node: 10.5.0_506ca6ef959d35afcce359030b1bc9ff
tslib: 2.3.1
typescript: 4.6.2
tools/monorepo-merge:
specifiers:
'@oclif/core': ^1
@ -1802,7 +1829,7 @@ packages:
/@babel/code-frame/7.12.11:
resolution: {integrity: sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw==}
dependencies:
'@babel/highlight': 7.16.0
'@babel/highlight': 7.16.10
dev: true
/@babel/code-frame/7.16.0:
@ -8234,7 +8261,7 @@ packages:
ignore: 4.0.6
import-fresh: 3.3.0
js-yaml: 3.14.1
minimatch: 3.0.4
minimatch: 3.1.2
strip-json-comments: 3.1.1
transitivePeerDependencies:
- supports-color
@ -8332,7 +8359,7 @@ packages:
dependencies:
'@humanwhocodes/object-schema': 1.2.1
debug: 4.3.4
minimatch: 3.0.4
minimatch: 3.1.2
transitivePeerDependencies:
- supports-color
dev: true
@ -9931,11 +9958,11 @@ packages:
chalk: 4.1.2
clean-stack: 3.0.1
cli-progress: 3.10.0
debug: 4.3.3_supports-color@8.1.1
debug: 4.3.4_supports-color@8.1.1
ejs: 3.1.6
fs-extra: 9.1.0
get-package-type: 0.1.0
globby: 11.0.4
globby: 11.1.0
hyperlinker: 1.0.0
indent-string: 4.0.0
is-wsl: 2.2.0
@ -9979,7 +10006,7 @@ packages:
'@oclif/color': 1.0.1
'@oclif/core': 1.3.4
chalk: 4.1.2
debug: 4.3.3
debug: 4.3.4
fs-extra: 9.1.0
http-call: 5.3.0
load-json-file: 5.3.0
@ -9997,7 +10024,7 @@ packages:
dependencies:
'@oclif/core': 1.3.4
chalk: 4.1.2
debug: 4.3.3
debug: 4.3.4
fs-extra: 9.1.0
http-call: 5.3.0
lodash: 4.17.21
@ -13617,7 +13644,7 @@ packages:
resolution: {integrity: sha512-ayJ0iOCDNHnKpKTgBG6Q6JOnHTj9zFta+3j2b8Ejza0e4cvRyMn0ZoLEmbPrTHe5YYRlDYPvPWVdV4cTaRyH7g==}
dependencies:
'@types/expect': 1.20.4
'@types/node': 16.10.3
'@types/node': 17.0.21
dev: true
/@types/webpack-env/1.16.3:
@ -13686,7 +13713,6 @@ packages:
re-resizable: 4.11.0
transitivePeerDependencies:
- react
- react-dom
dev: true
/@types/wordpress__compose/4.0.1:
@ -16678,7 +16704,7 @@ packages:
resolution: {integrity: sha512-Zn4cw2NEqd+9fiSVWMscnjyQ1a8Yfoc5oBajLeo5w+YBHgDUcEBY2hS4YpTz6iN5f/2zQiktcuM6tS8x1p9dpA==}
engines: {node: '>= 8.0.0'}
dependencies:
debug: 4.3.3
debug: 4.3.4
depd: 1.1.2
humanize-ms: 1.2.1
transitivePeerDependencies:
@ -20530,18 +20556,6 @@ packages:
dependencies:
ms: 2.1.2
/debug/4.3.3_supports-color@8.1.1:
resolution: {integrity: sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==}
engines: {node: '>=6.0'}
peerDependencies:
supports-color: '*'
peerDependenciesMeta:
supports-color:
optional: true
dependencies:
ms: 2.1.2
supports-color: 8.1.1
/debug/4.3.3_supports-color@9.2.2:
resolution: {integrity: sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==}
engines: {node: '>=6.0'}
@ -20566,6 +20580,18 @@ packages:
dependencies:
ms: 2.1.2
/debug/4.3.4_supports-color@8.1.1:
resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==}
engines: {node: '>=6.0'}
peerDependencies:
supports-color: '*'
peerDependenciesMeta:
supports-color:
optional: true
dependencies:
ms: 2.1.2
supports-color: 8.1.1
/debuglog/1.0.1:
resolution: {integrity: sha1-qiT/uaw9+aI1GDfPstJ5NgzXhJI=}
dev: true
@ -22432,7 +22458,7 @@ packages:
ajv: 6.12.6
chalk: 4.1.2
cross-spawn: 7.0.3
debug: 4.3.3
debug: 4.3.4
doctrine: 3.0.0
enquirer: 2.3.6
escape-string-regexp: 4.0.0
@ -22455,7 +22481,7 @@ packages:
json-stable-stringify-without-jsonify: 1.0.1
levn: 0.4.1
lodash.merge: 4.6.2
minimatch: 3.0.4
minimatch: 3.1.2
natural-compare: 1.4.0
optionator: 0.9.1
progress: 2.0.3
@ -22463,7 +22489,7 @@ packages:
semver: 7.3.5
strip-ansi: 6.0.1
strip-json-comments: 3.1.1
table: 6.7.3
table: 6.8.0
text-table: 0.2.0
v8-compile-cache: 2.3.0
transitivePeerDependencies:
@ -23282,7 +23308,7 @@ packages:
/filelist/1.0.2:
resolution: {integrity: sha512-z7O0IS8Plc39rTCq6i6iHxk43duYOn8uFJiWSewIq0Bww1RNybVHSCjahmcC87ZqAm4OTvFzlzeGu3XAzG1ctQ==}
dependencies:
minimatch: 3.0.4
minimatch: 3.1.2
/fileset/2.0.3:
resolution: {integrity: sha1-jnVIqW08wjJ+5eZ0FocjozO7oqA=}
@ -24341,9 +24367,9 @@ packages:
'@types/glob': 7.2.0
array-union: 2.1.0
dir-glob: 3.0.1
fast-glob: 3.2.7
fast-glob: 3.2.11
glob: 7.2.0
ignore: 5.1.9
ignore: 5.2.0
merge2: 1.4.1
slash: 3.0.0
dev: true
@ -25155,7 +25181,7 @@ packages:
engines: {node: '>=8.0.0'}
dependencies:
content-type: 1.0.4
debug: 4.3.3
debug: 4.3.4
is-retry-allowed: 1.2.0
is-stream: 2.0.1
parse-json: 4.0.0
@ -25220,7 +25246,7 @@ packages:
dependencies:
'@tootallnate/once': 2.0.0
agent-base: 6.0.2
debug: 4.3.3
debug: 4.3.4
transitivePeerDependencies:
- supports-color
dev: true
@ -25687,7 +25713,7 @@ packages:
mute-stream: 0.0.8
ora: 5.4.1
run-async: 2.4.1
rxjs: 7.5.4
rxjs: 7.5.5
string-width: 4.2.3
strip-ansi: 6.0.1
through: 2.3.8
@ -26571,7 +26597,7 @@ packages:
async: 0.9.2
chalk: 2.4.2
filelist: 1.0.2
minimatch: 3.0.4
minimatch: 3.1.2
/jest-allure/0.1.3:
resolution: {integrity: sha512-EkO3LmkPx/a4VDg81JKtdy6kFXI0D1rHRIJ5Sa9MZaGtFYpgiCJFa6XwTgIySaVN+3+QBbIt1558CEkIW7FKFA==}
@ -29172,7 +29198,7 @@ packages:
resolution: {integrity: sha512-cJGP40Jc/VXUsp8/OrnyKyTZ1y6v/dphm3bioS+RrKXjK2BB6wHUd6JptZEFDGgGahMT+InnZO5i1Ei9mpC8Bw==}
engines: {node: '>=6'}
dependencies:
graceful-fs: 4.2.8
graceful-fs: 4.2.9
parse-json: 4.0.0
pify: 4.0.1
strip-bom: 3.0.0
@ -29497,6 +29523,7 @@ packages:
/lru-cache/7.4.0:
resolution: {integrity: sha512-YOfuyWa/Ee+PXbDm40j9WXyJrzQUynVbgn4Km643UYcWNcrSfRkKL0WaiUcxcIbkXcVTgNpDqSnPXntWXT75cw==}
engines: {node: '>=12'}
deprecated: Please update to latest patch version to fix memory leak https://github.com/isaacs/node-lru-cache/issues/227
dev: true
/lru/3.1.0:
@ -29807,7 +29834,7 @@ packages:
commondir: 1.0.1
deep-extend: 0.6.0
ejs: 3.1.6
globby: 11.0.4
globby: 11.1.0
isbinaryfile: 4.0.8
mem-fs: 2.2.1
minimatch: 3.1.2
@ -31084,7 +31111,7 @@ packages:
'@oclif/plugin-warn-if-update-available': 2.0.4
aws-sdk: 2.1079.0
concurrently: 7.0.0
debug: 4.3.3
debug: 4.3.4
find-yarn-workspace-root: 2.0.0
fs-extra: 8.1.0
github-slugger: 1.4.0
@ -31380,7 +31407,7 @@ packages:
resolution: {integrity: sha512-UJKdSzgd3KOnXXAtqN5+/eeHcvTn1hBkesEmElVgvO/NAYcxAvmjzIGmnNd3Tb/gRAvMBdNRFD4qAWdHxY6QXg==}
engines: {node: '>=12.10.0'}
dependencies:
debug: 4.3.3
debug: 4.3.4
p-queue: 6.6.2
transitivePeerDependencies:
- supports-color
@ -33424,7 +33451,7 @@ packages:
engines: {node: '>=8.0.0'}
dependencies:
chalk: 2.4.2
debug: 4.3.3
debug: 4.3.4
execa: 0.10.0
fs-extra: 6.0.1
get-stream: 5.2.0
@ -35284,12 +35311,6 @@ packages:
dependencies:
tslib: 1.14.1
/rxjs/7.5.4:
resolution: {integrity: sha512-h5M3Hk78r6wAheJF0a5YahB1yRQKCsZ4MsGdZ5O9ETbVtjPcScGfrMmoOq7EBsCRzd4BDkvDJ7ogP8Sz5tTFiQ==}
dependencies:
tslib: 2.3.1
dev: true
/rxjs/7.5.5:
resolution: {integrity: sha512-sy+H0pQofO95VDmFLzyaw9xNJU4KTRSwQIGM6+iG3SypAtCiLDzpeG8sJrNCWn2Up9km+KhkvTdbkrdy+yzZdw==}
dependencies:
@ -35870,7 +35891,7 @@ packages:
engines: {node: '>= 10'}
dependencies:
agent-base: 6.0.2
debug: 4.3.3
debug: 4.3.4
socks: 2.6.2
transitivePeerDependencies:
- supports-color
@ -37720,6 +37741,37 @@ packages:
webpack: 5.70.0
dev: true
/ts-node/10.5.0_506ca6ef959d35afcce359030b1bc9ff:
resolution: {integrity: sha512-6kEJKwVxAJ35W4akuiysfKwKmjkbYxwQMTBaAxo9KKAx/Yd26mPUyhGz3ji+EsJoAgrLqVsYHNuuYwQe22lbtw==}
hasBin: true
peerDependencies:
'@swc/core': '>=1.2.50'
'@swc/wasm': '>=1.2.50'
'@types/node': '*'
typescript: '>=2.7'
peerDependenciesMeta:
'@swc/core':
optional: true
'@swc/wasm':
optional: true
dependencies:
'@cspotcode/source-map-support': 0.7.0
'@tsconfig/node10': 1.0.8
'@tsconfig/node12': 1.0.9
'@tsconfig/node14': 1.0.1
'@tsconfig/node16': 1.0.2
'@types/node': 16.10.3
acorn: 8.7.0
acorn-walk: 8.2.0
arg: 4.1.3
create-require: 1.1.1
diff: 4.0.2
make-error: 1.3.6
typescript: 4.6.2
v8-compile-cache-lib: 3.0.0
yn: 3.1.1
dev: true
/ts-node/10.5.0_e0d88945dfc7787883e9c330c9196a96:
resolution: {integrity: sha512-6kEJKwVxAJ35W4akuiysfKwKmjkbYxwQMTBaAxo9KKAx/Yd26mPUyhGz3ji+EsJoAgrLqVsYHNuuYwQe22lbtw==}
hasBin: true
@ -37740,7 +37792,7 @@ packages:
'@tsconfig/node14': 1.0.1
'@tsconfig/node16': 1.0.2
'@types/node': 16.10.3
acorn: 8.5.0
acorn: 8.7.0
acorn-walk: 8.2.0
arg: 4.1.3
create-require: 1.1.1
@ -39823,13 +39875,13 @@ packages:
cli-table: 0.3.11
commander: 7.1.0
dateformat: 4.6.3
debug: 4.3.3
debug: 4.3.4
diff: 5.0.0
error: 10.4.0
escape-string-regexp: 4.0.0
execa: 5.1.1
find-up: 5.0.0
globby: 11.0.4
globby: 11.1.0
grouped-queue: 2.0.0
inquirer: 8.2.0
is-scoped: 2.1.0
@ -39837,7 +39889,7 @@ packages:
log-symbols: 4.1.0
mem-fs: 2.2.1
mem-fs-editor: 9.4.0_mem-fs@2.2.1
minimatch: 3.0.4
minimatch: 3.1.2
npmlog: 5.0.1
p-queue: 6.6.2
p-transform: 1.3.0
@ -39865,7 +39917,7 @@ packages:
dependencies:
chalk: 4.1.2
dargs: 7.0.0
debug: 4.3.3
debug: 4.3.4
execa: 4.1.0
github-username: 6.0.0
lodash: 4.17.21

View File

@ -2,3 +2,4 @@ packages:
- 'packages/js/**'
- 'plugins/**'
- 'tools/monorepo-merge'
- 'tools/code-analyzer'

View File

@ -0,0 +1 @@
/dist

View File

@ -0,0 +1,9 @@
{
"parser": "@typescript-eslint/parser",
"ignorePatterns": [ "dist/", "node_modules/" ],
"plugins": [ "@typescript-eslint/eslint-plugin" ],
"extends": [ "plugin:@wordpress/eslint-plugin/recommended-with-formatting" ],
"rules": {
"no-unused-vars": "off"
}
}

1
tools/code-analyzer/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/oclif.manifest.json

17
tools/code-analyzer/bin/dev Executable file
View File

@ -0,0 +1,17 @@
#!/usr/bin/env node
const oclif = require('@oclif/core')
const path = require('path')
const project = path.join(__dirname, '..', 'tsconfig.json')
// In dev mode -> use ts-node and dev plugins
process.env.NODE_ENV = 'development'
require('ts-node').register({project})
// In dev mode, always show stack traces
oclif.settings.debug = true;
// Start the CLI
oclif.run().then(oclif.flush).catch(oclif.Errors.handle)

View File

@ -0,0 +1,3 @@
@echo off
node "%~dp0\dev" %*

5
tools/code-analyzer/bin/run Executable file
View File

@ -0,0 +1,5 @@
#!/usr/bin/env node
const oclif = require('@oclif/core')
oclif.run().then(require('@oclif/core/flush')).catch(require('@oclif/core/handle'))

View File

@ -0,0 +1,3 @@
@echo off
node "%~dp0\run" %*

View File

@ -0,0 +1,60 @@
{
"name": "code-analyzer",
"version": "0.0.0",
"description": "A tool to analyze code changes in WooCommerce Monorepo.",
"author": "Automattic",
"bin": {
"code-analyzer": "./bin/run"
},
"homepage": "https://github.com/woocommerce/woocommerce",
"license": "GPLv2",
"main": "dist/index.js",
"repository": "woocommerce/woocommerce",
"files": [
"/bin",
"/dist",
"/npm-shrinkwrap.json",
"/oclif.manifest.json"
],
"dependencies": {
"@oclif/core": "^1",
"@oclif/plugin-help": "^5",
"@oclif/plugin-plugins": "^2.0.1"
},
"devDependencies": {
"@types/node": "^16.9.4",
"eslint": "^7.32.0",
"globby": "^11",
"oclif": "^2",
"shx": "^0.3.3",
"ts-node": "^10.2.1",
"tslib": "^2.3.1",
"typescript": "^4.4.3"
},
"oclif": {
"bin": "code-analyzer",
"dirname": "code-analyzer",
"commands": "./dist/commands",
"plugins": [
"@oclif/plugin-help",
"@oclif/plugin-plugins"
],
"topicSeparator": " ",
"topics": {
"analyzer": {
"description": "Analyzes code changes in the monorepo."
}
}
},
"scripts": {
"build": "shx rm -rf dist && tsc -b",
"lint": "eslint . --ext .ts --config .eslintrc",
"postpack": "shx rm -f oclif.manifest.json",
"posttest": "pnpm lint",
"prepack": "pnpm build && oclif manifest"
},
"engines": {
"node": ">=12.0.0"
},
"types": "dist/index.d.ts"
}

View File

@ -0,0 +1,553 @@
/**
* External dependencies
*/
import { CliUx, Command, Flags } from '@oclif/core';
import { tmpdir } from 'os';
import { join } from 'path';
import { readFileSync } from 'fs';
import { execSync } from 'child_process';
/**
* Internal dependencies
*/
import { MONOREPO_ROOT } from '../../const';
/**
* Analyzer class
*/
export default class Analyzer extends Command {
/**
* CLI description
*/
static description = 'Analyze code changes in WooCommerce Monorepo.';
/**
* CLI arguments
*/
static args = [
{
name: 'compare',
description:
'GitHub branch or commit hash to compare against the base branch/commit.',
required: true,
},
];
/**
* CLI flags.
*/
static flags = {
base: Flags.string( {
char: 'b',
description: 'GitHub base branch or commit hash.',
default: 'trunk',
} ),
output: Flags.string( {
char: 'o',
description: 'Output styling.',
options: [ 'console', 'github' ],
default: 'console',
} ),
source: Flags.string( {
char: 's',
description: 'GitHub organization/repository.',
default: 'woocommerce/woocommerce',
} ),
plugin: Flags.string( {
char: 'p',
description: 'Plugin to check for',
options: [ 'core', 'admin', 'beta' ],
default: 'core',
} ),
};
/**
* This method is called to execute the command
*/
async run(): Promise< void > {
const { args, flags } = await this.parse( Analyzer );
await this.validateArgs( flags.source );
const patchContent = await this.getChanges(
flags.source,
args.compare,
flags.base
);
const pluginData = await this.getPluginData( flags.plugin );
this.log( `${ pluginData[ 1 ] } Version: ${ pluginData[ 0 ] }` );
await this.scanChanges( patchContent, pluginData[ 0 ], flags.output );
}
/**
* Validates all of the arguments to make sure
*
* @param {string} source The GitHub repository we are merging.
*/
private async validateArgs( source: string ): Promise< void > {
// We only support pulling from GitHub so the format needs to match that.
if ( ! source.match( /^[a-z0-9\-]+\/[a-z0-9\-]+$/ ) ) {
this.error(
'The "source" argument must be in "organization/repository" format'
);
}
}
/**
* Get plugin data
*
* @param {string} plugin Plugin slug.
* @return {Promise<string[]>} Promise.
*/
private async getPluginData( plugin: string ): Promise< string[] > {
/**
* List of plugins from our monorepo.
*/
const plugins = <any>{
core: {
name: 'WooCommerce',
mainFile: join(
MONOREPO_ROOT,
'plugins',
'woocommerce',
'woocommerce.php'
),
},
admin: {
name: 'WooCommerce Admin',
mainFile: join(
MONOREPO_ROOT,
'plugins',
'woocommerce-admin',
'woocommerce-admin.php'
),
},
beta: {
name: 'WooCommerce Beta Tester',
mainFile: join(
MONOREPO_ROOT,
'plugins',
'woocommerce-beta-tester',
'woocommerce-beta-tester.php'
),
},
};
const pluginData = plugins[ plugin ];
CliUx.ux.action.start( `Getting ${ pluginData.name } version` );
const content = readFileSync( pluginData.mainFile ).toString();
const rawVer = content.match( /^\s+\*\s+Version:\s+(.*)/m );
if ( ! rawVer ) {
this.error( 'Failed to find plugin version!' );
}
const version = rawVer[ 1 ].replace( /\-.*/, '' );
CliUx.ux.action.stop();
return [ version, pluginData.name, pluginData.mainFile ];
}
/**
* Fetch branches from origin.
*
* @param {string} branch branch/commit hash.
* @return {Promise<boolean>} Promise.
*/
private async fetchBranch( branch: string ): Promise< boolean > {
CliUx.ux.action.start( `Fetching ${ branch }` );
const branches = execSync( 'git branch', {
encoding: 'utf-8',
} );
const branchExistsLocally = branches.includes( branch );
if ( branchExistsLocally ) {
CliUx.ux.action.stop();
return true;
}
try {
// Fetch branch.
execSync( `git fetch origin ${ branch }` );
// Create branch.
execSync( `git branch ${ branch } origin/${ branch }` );
} catch ( e ) {
this.error( `Unable to fetch ${ branch }` );
}
CliUx.ux.action.stop();
return true;
}
/**
* Generate a patch file into the temp directory and return its contents
*
* @param {string} source The GitHub repository.
* @param {string} compare Branch/commit hash to compare against the base.
* @param {string} base Base branch/commit hash.
* @return {Promise<string>} Promise.
*/
private async getChanges(
source: string,
compare: string,
base: string
): Promise< string > {
const filename = `${ source }-${ base }-${ compare }.patch`.replace(
/\//g,
'-'
);
const filepath = join( tmpdir(), filename );
await this.fetchBranch( base );
await this.fetchBranch( compare );
CliUx.ux.action.start( 'Generating patch for ' + compare );
try {
const diffCommand = `git diff ${ base }...${ compare } > ${ filepath }`;
execSync( diffCommand );
} catch ( e ) {
this.error(
'Unable to create diff. Check that git origin, base branch, and compare branch all exist.'
);
}
const content = readFileSync( filepath ).toString();
CliUx.ux.action.stop();
return content;
}
/**
* Get patches
*
* @param {string} content Patch content.
* @param {RegExp} regex Regex to find specific patches.
* @return {Promise<string[]>} Promise.
*/
private async getPatches(
content: string,
regex: RegExp
): Promise< string[] > {
const patches = content.split( 'diff --git ' );
const changes: string[] = [];
for ( const p in patches ) {
const patch = patches[ p ];
const id = patch.match( regex );
if ( id ) {
changes.push( patch );
}
}
return changes;
}
/**
* Get filename from patch
*
* @param {string} str String to extract filename from.
* @return {Promise<string>} Promise.
*/
private async getFilename( str: string ): Promise< string > {
return str.replace( /^a(.*)\s.*/, '$1' );
}
/**
* Format version string for regex.
*
* @param {string} rawVersion Raw version number.
* @return {Promise<string>} Promise.
*/
private async getVersionRegex( rawVersion: string ): Promise< string > {
const version = rawVersion.replace( /\./g, '\\.' );
if ( rawVersion.endsWith( '.0' ) ) {
return version + '|' + version.slice( 0, -3 ) + '\\n';
}
return version;
}
/**
* Scan patches for changes in templates, hooks and database schema
*
* @param {string} content Patch content.
* @param {string} version Current product version.
* @param {string} output Output style.
*/
private async scanChanges(
content: string,
version: string,
output: string
): Promise< void > {
const templates = await this.scanTemplates( content, version );
const hooks = await this.scanHooks( content, version, output );
// @todo: Scan for changes to database schema.
if ( templates.size ) {
await this.printTemplateResults(
templates,
output,
'TEMPLATE CHANGES'
);
} else {
this.log( 'No template changes found' );
}
if ( hooks.size ) {
await this.printHookResults( hooks, output, 'HOOKS' );
} else {
this.log( 'No new hooks found' );
}
}
/**
* Print template results
*
* @param {Map<string, string[]>} data Raw data.
* @param {string} output Output style.
* @param {string} title Section title.
*/
private async printTemplateResults(
data: Map< string, string[] >,
output: string,
title: string
): Promise< void > {
if ( output === 'github' ) {
let opt = '\\n\\n### Template changes:';
for ( const [ key, value ] of data ) {
opt += `\\n* **file:** ${ key }`;
opt += `\\n * ${ value[ 0 ].toUpperCase() }: ${ value[ 2 ] }`;
this.log(
`::${ value[ 0 ] } file=${ key },line=1,title=${ value[ 1 ] }::${ value[ 2 ] }`
);
}
this.log( `::set-output name=templates::${ opt }` );
} else {
this.log( `\n## ${ title }:` );
for ( const [ key, value ] of data ) {
this.log( 'FILE: ' + key );
this.log(
'---------------------------------------------------'
);
this.log(
` ${ value[ 0 ].toUpperCase() } | ${ value[ 1 ] } | ${
value[ 2 ]
}`
);
this.log(
'---------------------------------------------------'
);
}
}
}
/**
* Print hook results
*
* @param {Map} data Raw data.
* @param {string} output Output style.
* @param {string} title Section title.
*/
private async printHookResults(
data: Map< string, Map< string, string[] > >,
output: string,
title: string
): Promise< void > {
if ( output === 'github' ) {
let opt = '\\n\\n### New hooks:';
for ( const [ key, value ] of data ) {
if ( value.size ) {
opt += `\\n* **file:** ${ key }`;
for ( const [ k, v ] of value ) {
opt += `\\n * ${ v[ 0 ].toUpperCase() }: ${ v[ 2 ] }`;
this.log(
`::${ v[ 0 ] } file=${ key },line=1,title=${ v[ 1 ] } - ${ k }::${ v[ 2 ] }`
);
}
}
}
this.log( `::set-output name=wphooks::${ opt }` );
} else {
this.log( `\n## ${ title }:` );
for ( const [ key, value ] of data ) {
if ( value.size ) {
this.log( 'FILE: ' + key );
this.log(
'---------------------------------------------------'
);
for ( const [ k, v ] of value ) {
this.log( 'HOOK: ' + k );
this.log(
'---------------------------------------------------'
);
this.log(
` ${ v[ 0 ].toUpperCase() } | ${ v[ 1 ] } | ${
v[ 2 ]
}`
);
this.log(
'---------------------------------------------------'
);
}
}
}
}
}
/**
* Get hook name.
*
* @param {string} name Raw hook name.
* @return {Promise<string>} Promise.
*/
private async getHookName( name: string ): Promise< string > {
if ( name.indexOf( ',' ) > -1 ) {
name = name.substring( 0, name.indexOf( ',' ) );
}
return name.replace( /(\'|\")/g, '' ).trim();
}
/**
* Scan patches for changes in templates
*
* @param {string} content Patch content.
* @param {string} version Current product version.
* @return {Promise<Map<string, string[]>>} Promise.
*/
private async scanTemplates(
content: string,
version: string
): Promise< Map< string, string[] > > {
CliUx.ux.action.start( 'Scanning template changes' );
const report: Map< string, string[] > = new Map< string, string[] >();
if ( ! content.match( /diff --git a\/(.+)\/templates\/(.+)/g ) ) {
CliUx.ux.action.stop();
return report;
}
const matchPatches = /^a\/(.+)\/templates\/(.+)/g;
const title = 'Template change detected';
const patches = await this.getPatches( content, matchPatches );
const matchVersion = `^(\\+.+\\*.+)(@version)\\s+(${ version.replace(
/\./g,
'\\.'
) }).*`;
const versionRegex = new RegExp( matchVersion, 'g' );
for ( const p in patches ) {
const patch = patches[ p ];
const lines = patch.split( '\n' );
const filepath = await this.getFilename( lines[ 0 ] );
let code = 'warning';
let message = 'This template may require a version bump!';
for ( const l in lines ) {
const line = lines[ l ];
if ( line.match( versionRegex ) ) {
code = 'notice';
message = 'Version bump found';
}
}
if ( code === 'notice' && report.get( filepath ) ) {
report.set( filepath, [ code, title, message ] );
} else if ( ! report.get( filepath ) ) {
report.set( filepath, [ code, title, message ] );
}
}
CliUx.ux.action.stop();
return report;
}
/**
* Scan patches for hooks
*
* @param {string} content Patch content.
* @param {string} version Current product version.
* @param {string} output Output style.
* @return {Promise<Map<string, Map<string, string[]>>>} Promise.
*/
private async scanHooks(
content: string,
version: string,
output: string
): Promise< Map< string, Map< string, string[] > > > {
CliUx.ux.action.start( 'Scanning for new hooks' );
const report: Map< string, Map< string, string[] > > = new Map<
string,
Map< string, string[] >
>();
if ( ! content.match( /diff --git a\/(.+).php/g ) ) {
CliUx.ux.action.stop();
return report;
}
const matchPatches = /^a\/(.+).php/g;
const patches = await this.getPatches( content, matchPatches );
const verRegEx = await this.getVersionRegex( version );
const matchHooks = `@since\\s+(${ verRegEx })(.*?)(apply_filters|do_action)\\((\\s+)?(\\'|\\")(.*?)(\\'|\\")`;
const newRegEx = new RegExp( matchHooks, 'gs' );
for ( const p in patches ) {
const patch = patches[ p ];
const results = patch.match( newRegEx );
const hooksList: Map< string, string[] > = new Map<
string,
string[]
>();
if ( ! results ) {
continue;
}
const lines = patch.split( '\n' );
const filepath = await this.getFilename( lines[ 0 ] );
for ( const raw of results ) {
// Extract hook name and type.
const hookName = raw.match(
/(.*)(do_action|apply_filters)\(\s+'(.*)'/
);
if ( ! hookName ) {
continue;
}
const name = await this.getHookName( hookName[ 3 ] );
const kind =
hookName[ 2 ] === 'do_action' ? 'action' : 'filter';
const CLIMessage = `\'${ name }\' introduced in ${ version }`;
const GithubMessage = `\\'${ name }\\' introduced in ${ version }`;
const message =
output === 'github' ? GithubMessage : CLIMessage;
const title = `New ${ kind } found`;
if ( ! hookName[ 2 ].startsWith( '-' ) ) {
hooksList.set( name, [ 'NOTICE', title, message ] );
}
}
report.set( filepath, hooksList );
}
CliUx.ux.action.stop();
return report;
}
}

View File

@ -0,0 +1,7 @@
/**
* External dependencies
*/
import { dirname } from 'path';
// Escape from ./tools/monorepo-merge/src
export const MONOREPO_ROOT = dirname( dirname( dirname( __dirname ) ) );

View File

@ -0,0 +1 @@
export { run } from '@oclif/core';

View File

@ -0,0 +1,14 @@
{
"compilerOptions": {
"declaration": true,
"importHelpers": true,
"module": "commonjs",
"outDir": "dist",
"rootDir": "src",
"strict": true,
"target": "es2019"
},
"include": [
"src/**/*"
]
}