Extract the scanning functionality from code-analyzer, move common code into a separate package (#34600)
Also add contributor command and auto tagging to release-post-generator (#34608)
This commit is contained in:
parent
03b9032de8
commit
614d98ff60
|
@ -13,12 +13,12 @@ jobs:
|
|||
run: |
|
||||
npm install -g pnpm@^6.24.2
|
||||
npm -g i @wordpress/env@5.1.0
|
||||
pnpm install --filter code-analyzer
|
||||
pnpm install --filter code-analyzer --filter cli-core
|
||||
- name: Run analyzer
|
||||
id: run
|
||||
run: |
|
||||
version=$(./tools/code-analyzer/bin/dev major_minor "${{ github.head_ref || github.ref_name }}" "plugins/woocommerce/woocommerce.php")
|
||||
./tools/code-analyzer/bin/dev analyzer "$GITHUB_HEAD_REF" $version
|
||||
version=$(pnpm run analyzer --filter code-analyzer -- major-minor "${{ github.head_ref || github.ref_name }}" "plugins/woocommerce/woocommerce.php" | tail -n 1)
|
||||
pnpm run analyzer --filter code-analyzer -- "$GITHUB_HEAD_REF" $version -o "github"
|
||||
- name: Print results
|
||||
id: results
|
||||
run: echo "::set-output name=results::${{ steps.run.outputs.templates }}${{ steps.run.outputs.wphooks }}${{ steps.run.outputs.schema }}${{ steps.run.outputs.database }}"
|
||||
|
|
6929
pnpm-lock.yaml
6929
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
|
@ -7,5 +7,6 @@ packages:
|
|||
- 'tools/create-extension'
|
||||
- 'tools/package-release'
|
||||
- 'tools/cherry-pick'
|
||||
- 'tools/release-post-generator'
|
||||
- 'tools/release-posts'
|
||||
- 'tools/cli-core'
|
||||
- 'tools/version-bump'
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
### CLI Core
|
||||
|
||||
This package contains utilities and libraries providing core functionality to
|
||||
cli tools within `tools` so that this functionality can be easily reused.
|
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"name": "cli-core",
|
||||
"version": "0.0.1",
|
||||
"description": "Core functionality for CLI tools/commands.",
|
||||
"main": " ",
|
||||
"scripts": {},
|
||||
"author": "Automattic",
|
||||
"license": "GPL-2.0-or-later",
|
||||
"devDependencies": {
|
||||
"@tsconfig/node16": "^1.0.3",
|
||||
"typescript": "^4.7.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"chalk": "^4.1.2",
|
||||
"dotenv": "^10.0.0",
|
||||
"ora": "^5.4.1",
|
||||
"simple-git": "^3.10.0",
|
||||
"ts-node": "^10.9.1",
|
||||
"uuid": "^8.3.2"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,232 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { execSync } from 'child_process';
|
||||
import { join } from 'path';
|
||||
import { tmpdir } from 'os';
|
||||
import { mkdirSync } from 'fs';
|
||||
import { simpleGit } from 'simple-git';
|
||||
import { v4 } from 'uuid';
|
||||
import { mkdir, rm } from 'fs/promises';
|
||||
import { URL } from 'node:url';
|
||||
|
||||
/**
|
||||
* Get filename from patch
|
||||
*
|
||||
* @param {string} str String to extract filename from.
|
||||
* @return {string} formatted filename.
|
||||
*/
|
||||
export const getFilename = ( str: string ): string => {
|
||||
return str.replace( /^a(.*)\s.*/, '$1' );
|
||||
};
|
||||
|
||||
/**
|
||||
* Get patches
|
||||
*
|
||||
* @param {string} content Patch content.
|
||||
* @param {RegExp} regex Regex to find specific patches.
|
||||
* @return {string[]} Array of patches.
|
||||
*/
|
||||
export const getPatches = ( content: string, regex: RegExp ): 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;
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if a string is a valid url.
|
||||
*
|
||||
* @param {string} maybeURL - the URL string to check
|
||||
* @return {boolean} whether the string is a valid URL or not.
|
||||
*/
|
||||
const isUrl = ( maybeURL: string ) => {
|
||||
try {
|
||||
new URL( maybeURL );
|
||||
return true;
|
||||
} catch ( e ) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Clone a git repository.
|
||||
*
|
||||
* @param {string} repoPath - the path (either URL or file path) to the repo to clone.
|
||||
* @return {Promise<string>} the path to the cloned repo.
|
||||
*/
|
||||
export const cloneRepo = async ( repoPath: string ) => {
|
||||
const folderPath = join( tmpdir(), 'code-analyzer-tmp', v4() );
|
||||
mkdirSync( folderPath, { recursive: true } );
|
||||
|
||||
const git = simpleGit( { baseDir: folderPath } );
|
||||
await git.clone( repoPath, folderPath );
|
||||
|
||||
// If this is a local clone then the simplest way to maintain remote settings is to copy git config across
|
||||
if ( ! isUrl( repoPath ) ) {
|
||||
execSync( `cp ${ repoPath }/.git/config ${ folderPath }/.git/config` );
|
||||
}
|
||||
|
||||
// Update the repo.
|
||||
await git.fetch();
|
||||
|
||||
return folderPath;
|
||||
};
|
||||
|
||||
/**
|
||||
* Do a minimal sparse checkout of a github repo.
|
||||
*
|
||||
* @param {string} githubRepoUrl - the URL to the repo to checkout.
|
||||
* @param {string} path - the path to checkout to.
|
||||
* @param {Array<string>} directories - the files or directories to checkout.
|
||||
* @return {Promise<string>} the path to the cloned repo.
|
||||
*/
|
||||
export const sparseCheckoutRepo = async (
|
||||
githubRepoUrl: string,
|
||||
path: string,
|
||||
directories: string[]
|
||||
) => {
|
||||
const folderPath = join( tmpdir(), path );
|
||||
|
||||
// clean up if it already exists.
|
||||
await rm( folderPath, { recursive: true, force: true } );
|
||||
await mkdir( folderPath, { recursive: true } );
|
||||
|
||||
const git = simpleGit( { baseDir: folderPath } );
|
||||
|
||||
await git.clone( githubRepoUrl, folderPath );
|
||||
await git.raw( 'sparse-checkout', 'init', { '--cone': null } );
|
||||
await git.raw( 'sparse-checkout', 'set', directories.join( ' ' ) );
|
||||
|
||||
return folderPath;
|
||||
};
|
||||
|
||||
/**
|
||||
* checkoutRef - checkout a ref in a git repo.
|
||||
*
|
||||
* @param {string} pathToRepo - the path to the repo to checkout a ref from.
|
||||
* @param {string} ref - the ref to checkout.
|
||||
* @return {Response<string>} - the simple-git response.
|
||||
*/
|
||||
export const checkoutRef = ( pathToRepo: string, ref: string ) => {
|
||||
const git = simpleGit( { baseDir: pathToRepo } );
|
||||
return git.checkout( ref );
|
||||
};
|
||||
|
||||
/**
|
||||
* Do a git diff of 2 commit hashes (or branches)
|
||||
*
|
||||
* @param {string} baseDir - baseDir that the repo is in
|
||||
* @param {string} hashA - either a git commit hash or a git branch
|
||||
* @param {string} hashB - either a git commit hash or a git branch
|
||||
* @return {Promise<string>} - diff of the changes between the 2 hashes
|
||||
*/
|
||||
export const diffHashes = ( baseDir: string, hashA: string, hashB: string ) => {
|
||||
const git = simpleGit( { baseDir } );
|
||||
return git.diff( [ `${ hashA }..${ hashB }` ] );
|
||||
};
|
||||
|
||||
/**
|
||||
* Determines if a string is a commit hash or not.
|
||||
*
|
||||
* @param {string} ref - the ref to check
|
||||
* @return {boolean} whether the ref is a commit hash or not.
|
||||
*/
|
||||
const refIsHash = ( ref: string ) => {
|
||||
return /^[0-9a-f]{7,40}$/i.test( ref );
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the commit hash for a ref (either branch or commit hash). If a validly
|
||||
* formed hash is provided it is returned unmodified.
|
||||
*
|
||||
* @param {string} baseDir - the dir of the git repo to get the hash from.
|
||||
* @param {string} ref - Either a commit hash or a branch name.
|
||||
* @return {string} - the commit hash of the ref.
|
||||
*/
|
||||
export const getCommitHash = async ( baseDir: string, ref: string ) => {
|
||||
const isHash = refIsHash( ref );
|
||||
|
||||
// check if its in history, if its not an error will be thrown
|
||||
try {
|
||||
await simpleGit( { baseDir } ).show( ref );
|
||||
} catch ( e ) {
|
||||
throw new Error(
|
||||
`${ ref } is not a valid commit hash or branch name that exists in git history`
|
||||
);
|
||||
}
|
||||
|
||||
// If its not a hash we assume its a branch
|
||||
if ( ! isHash ) {
|
||||
return simpleGit( { baseDir } ).revparse( [ ref ] );
|
||||
}
|
||||
|
||||
// Its a hash already
|
||||
return ref;
|
||||
};
|
||||
|
||||
/**
|
||||
* generateDiff generates a diff for a given repo and 2 hashes or branch names.
|
||||
*
|
||||
* @param {string} tmpRepoPath - filepath to the repo to generate a diff from.
|
||||
* @param {string} hashA - commit hash or branch name.
|
||||
* @param {string} hashB - commit hash or branch name.
|
||||
* @param {Function} onError - the handler to call when an error occurs.
|
||||
*/
|
||||
export const generateDiff = async (
|
||||
tmpRepoPath: string,
|
||||
hashA: string,
|
||||
hashB: string,
|
||||
onError: ( error: string ) => void
|
||||
) => {
|
||||
try {
|
||||
const git = simpleGit( { baseDir: tmpRepoPath } );
|
||||
|
||||
const validBranches = [ hashA, hashB ].filter(
|
||||
( hash ) => ! refIsHash( hash )
|
||||
);
|
||||
|
||||
// checking out any branches will automatically track remote branches.
|
||||
for ( const validBranch of validBranches ) {
|
||||
// Note you can't do checkouts in parallel otherwise the git binary will crash
|
||||
await git.checkout( [ validBranch ] );
|
||||
}
|
||||
|
||||
// turn both hashes into commit hashes if they are not already.
|
||||
const commitHashA = await getCommitHash( tmpRepoPath, hashA );
|
||||
const commitHashB = await getCommitHash( tmpRepoPath, hashB );
|
||||
|
||||
const isRepo = await simpleGit( {
|
||||
baseDir: tmpRepoPath,
|
||||
} ).checkIsRepo();
|
||||
|
||||
if ( ! isRepo ) {
|
||||
throw new Error( 'Not a git repository' );
|
||||
}
|
||||
|
||||
const diff = await diffHashes( tmpRepoPath, commitHashA, commitHashB );
|
||||
|
||||
return diff;
|
||||
} catch ( e ) {
|
||||
if ( e instanceof Error ) {
|
||||
onError(
|
||||
`Unable to create diff. Check that git repo, base hash, and compare hash all exist.\n Error: ${ e.message }`
|
||||
);
|
||||
} else {
|
||||
onError(
|
||||
'Unable to create diff. Check that git repo, base hash, and compare hash all exist.'
|
||||
);
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
};
|
|
@ -0,0 +1,65 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import ora, { Ora } from 'ora';
|
||||
import chalk from 'chalk';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { getEnvVar } from './environment';
|
||||
|
||||
const LOGGING_LEVELS: Record< string, number > = {
|
||||
verbose: 3,
|
||||
warn: 2,
|
||||
error: 1,
|
||||
silent: 0,
|
||||
};
|
||||
|
||||
const { log, error, warn } = console;
|
||||
export class Logger {
|
||||
private static lastSpinner: Ora | null;
|
||||
private static get loggingLevel() {
|
||||
return LOGGING_LEVELS[
|
||||
getEnvVar( 'LOGGER_LEVEL' ) || 'warn'
|
||||
] as number;
|
||||
}
|
||||
|
||||
static error( message: string ) {
|
||||
if ( Logger.loggingLevel >= LOGGING_LEVELS.error ) {
|
||||
error( chalk.red( message ) );
|
||||
process.exit( 1 );
|
||||
}
|
||||
}
|
||||
|
||||
static warn( message: string ) {
|
||||
if ( Logger.loggingLevel >= LOGGING_LEVELS.warn ) {
|
||||
warn( chalk.yellow( message ) );
|
||||
}
|
||||
}
|
||||
|
||||
static notice( message: string ) {
|
||||
if ( Logger.loggingLevel > LOGGING_LEVELS.silent ) {
|
||||
log( chalk.green( message ) );
|
||||
}
|
||||
}
|
||||
|
||||
static startTask( message: string ) {
|
||||
if ( Logger.loggingLevel > LOGGING_LEVELS.silent ) {
|
||||
const spinner = ora( chalk.green( `${ message }...` ) ).start();
|
||||
Logger.lastSpinner = spinner;
|
||||
}
|
||||
}
|
||||
|
||||
static endTask() {
|
||||
if (
|
||||
Logger.loggingLevel > LOGGING_LEVELS.silent &&
|
||||
Logger.lastSpinner
|
||||
) {
|
||||
Logger.lastSpinner.succeed(
|
||||
`${ Logger.lastSpinner.text } complete.`
|
||||
);
|
||||
Logger.lastSpinner = null;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,172 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { createServer, Server } from 'net';
|
||||
import { join } from 'path';
|
||||
import { writeFile } from 'fs/promises';
|
||||
import { exec } from 'child_process';
|
||||
import { promisify } from 'util';
|
||||
|
||||
export const execAsync = promisify( exec );
|
||||
|
||||
/**
|
||||
* Format version string for regex.
|
||||
*
|
||||
* @param {string} rawVersion Raw version number.
|
||||
* @return {string} version regex.
|
||||
*/
|
||||
export const getVersionRegex = ( rawVersion: string ): string => {
|
||||
const version = rawVersion.replace( /\./g, '\\.' );
|
||||
|
||||
if ( rawVersion.endsWith( '.0' ) ) {
|
||||
return version + '|' + version.slice( 0, -3 ) + '\\n';
|
||||
}
|
||||
|
||||
return version;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get filename from patch
|
||||
*
|
||||
* @param {string} str String to extract filename from.
|
||||
* @return {string} formatted filename.
|
||||
*/
|
||||
export const getFilename = ( str: string ): string => {
|
||||
return str.replace( /^a(.*)\s.*/, '$1' );
|
||||
};
|
||||
|
||||
/**
|
||||
* Get patches
|
||||
*
|
||||
* @param {string} content Patch content.
|
||||
* @param {RegExp} regex Regex to find specific patches.
|
||||
* @return {string[]} Array of patches.
|
||||
*/
|
||||
export const getPatches = ( content: string, regex: RegExp ): 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;
|
||||
};
|
||||
|
||||
/**
|
||||
* Determine if the default port for wp-env is already taken. If so, see
|
||||
* https://developer.wordpress.org/block-editor/reference-guides/packages/packages-env/#2-check-the-port-number
|
||||
* for alternatives.
|
||||
*
|
||||
* @return {Promise<boolean>} if the port is being currently used.
|
||||
*/
|
||||
export const isWPEnvPortTaken = () => {
|
||||
return new Promise< boolean >( ( resolve, reject ) => {
|
||||
const test: Server = createServer()
|
||||
.once( 'error', ( err: { code: string } ) => {
|
||||
return err.code === 'EADDRINUSE'
|
||||
? resolve( true )
|
||||
: reject( err );
|
||||
} )
|
||||
.once( 'listening', () => {
|
||||
return test.once( 'close', () => resolve( false ) ).close();
|
||||
} )
|
||||
.listen( '8888' );
|
||||
} );
|
||||
};
|
||||
|
||||
/**
|
||||
* Start wp-env.
|
||||
*
|
||||
* @param {string} tmpRepoPath - path to the temporary repo to start wp-env from.
|
||||
* @param {Function} error - error print method.
|
||||
* @return {boolean} if starting the container succeeded.
|
||||
*/
|
||||
export const startWPEnv = async (
|
||||
tmpRepoPath: string,
|
||||
error: ( s: string ) => void
|
||||
) => {
|
||||
try {
|
||||
// Stop wp-env if its already running.
|
||||
await execAsync( 'wp-env stop', {
|
||||
cwd: join( tmpRepoPath, 'plugins/woocommerce' ),
|
||||
encoding: 'utf-8',
|
||||
} );
|
||||
} catch ( e ) {
|
||||
// If an error is produced here, it means wp-env is not initialized and therefore not running already.
|
||||
}
|
||||
|
||||
try {
|
||||
if ( await isWPEnvPortTaken() ) {
|
||||
throw new Error(
|
||||
'Unable to start wp-env. Make sure port 8888 is available or specify port number WP_ENV_PORT in .wp-env.override.json'
|
||||
);
|
||||
}
|
||||
|
||||
await execAsync( 'wp-env start', {
|
||||
cwd: join( tmpRepoPath, 'plugins/woocommerce' ),
|
||||
encoding: 'utf-8',
|
||||
} );
|
||||
return true;
|
||||
} catch ( e ) {
|
||||
let message = '';
|
||||
if ( e instanceof Error ) {
|
||||
message = e.message;
|
||||
error( message );
|
||||
}
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Stop wp-env.
|
||||
*
|
||||
* @param {string} tmpRepoPath - path to the temporary repo to stop wp-env from.
|
||||
* @param {Function} error - error print method.
|
||||
* @return {boolean} if stopping the container succeeded.
|
||||
*/
|
||||
export const stopWPEnv = async (
|
||||
tmpRepoPath: string,
|
||||
error: ( s: string ) => void
|
||||
): Promise< boolean > => {
|
||||
try {
|
||||
await execAsync( 'wp-env stop', {
|
||||
cwd: join( tmpRepoPath, 'plugins/woocommerce' ),
|
||||
encoding: 'utf-8',
|
||||
} );
|
||||
return true;
|
||||
} catch ( e ) {
|
||||
let message = '';
|
||||
if ( e instanceof Error ) {
|
||||
message = e.message;
|
||||
error( message );
|
||||
}
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Generate a JSON file with the data passed.
|
||||
*
|
||||
* @param filePath - path to the file to be created.
|
||||
* @param data - data to be written to the file.
|
||||
* @return {Promise<void>} - promise that resolves when the file is written.
|
||||
*/
|
||||
export const generateJSONFile = ( filePath: string, data: unknown ) => {
|
||||
const json = JSON.stringify(
|
||||
data,
|
||||
function replacer( _, value ) {
|
||||
if ( value instanceof Map ) {
|
||||
return Array.from( value.entries() );
|
||||
}
|
||||
return value;
|
||||
},
|
||||
2
|
||||
);
|
||||
return writeFile( filePath, json );
|
||||
};
|
|
@ -8,22 +8,21 @@
|
|||
|
||||
Currently there are just 2 commands:
|
||||
|
||||
1. `analyzer`. Analyzer serves 2 roles currently, as a linter for PRs to check if introduced hook/template/db changes have associated changelog entries and also to provide file output of changes between
|
||||
WooCommerce versions for the purpose of automating release processes (such as generating release posts.)
|
||||
1. `lint`. Analyzer is used as a linter for PRs to check if hook/template/db changes were introduced. It produces output either directly on CI or via GH actions `set-output`.
|
||||
|
||||
Here is an example `analyzer` command:
|
||||
|
||||
`./bin/dev analyzer release/6.8 "6.8.0" -b=release/6.7`
|
||||
`pnpm run analyzer -- lint "release/6.8" "6.8.0" -b release/6.7`
|
||||
|
||||
In this command we compare the `release/6.7` and `release/6.8` branches to find differences, and we're looking for changes introduced since `6.8.0` (using the `@since` tag).
|
||||
|
||||
To find out more about the other arguments to the command you can run `./bin/dev analyzer --help`
|
||||
To find out more about the other arguments to the command you can run `pnpm run analyzer -- --help`
|
||||
|
||||
2. `major_minor`. This simple CLI tool gives you the latest `.0` major/minor released version of a plugin's mainfile based on Woo release conventions.
|
||||
2. `major-minor`. This simple CLI tool gives you the latest `.0` major/minor released version of a plugin's mainfile based on Woo release conventions.
|
||||
|
||||
Here is an example `major_minor` command:
|
||||
Here is an example `major-minor` command:
|
||||
|
||||
`./bin/dev major_minor release/6.8 "plugins/woocommerce/woocommerce.php"`
|
||||
`pnpm run analyzer major-minor -- "release/6.8" "plugins/woocommerce/woocommerce.php"`
|
||||
|
||||
In this command we checkout the branch `release/6.8` and check the version of the woocommerce.php mainfile located at the path passed. Note that at the time of
|
||||
writing the main file in this particular branch reports `6.8.1` so the output of this command is `6.8.0`.
|
||||
|
|
|
@ -1,17 +0,0 @@
|
|||
#!/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)
|
|
@ -1,3 +0,0 @@
|
|||
@echo off
|
||||
|
||||
node "%~dp0\dev" %*
|
|
@ -1,5 +0,0 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
const oclif = require('@oclif/core')
|
||||
|
||||
oclif.run().then(require('@oclif/core/flush')).catch(require('@oclif/core/handle'))
|
|
@ -1,3 +0,0 @@
|
|||
@echo off
|
||||
|
||||
node "%~dp0\run" %*
|
|
@ -1,61 +1,32 @@
|
|||
{
|
||||
"name": "code-analyzer",
|
||||
"version": "0.0.0",
|
||||
"version": "0.0.1",
|
||||
"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",
|
||||
"@commander-js/extra-typings": "^0.1.0",
|
||||
"@tsconfig/node16": "^1.0.3",
|
||||
"@types/uuid": "^8.3.4",
|
||||
"cli-core": "workspace:*",
|
||||
"commander": "^9.4.0",
|
||||
"dotenv": "^10.0.0",
|
||||
"simple-git": "^3.10.0",
|
||||
"uuid": "^8.3.2"
|
||||
},
|
||||
"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": {
|
||||
"turbo:build": "shx rm -rf dist && tsc -b",
|
||||
"build": "pnpm -w exec turbo run turbo:build --filter=$npm_package_name -- --",
|
||||
"lint": "eslint . --ext .ts --config .eslintrc",
|
||||
"postpack": "shx rm -f oclif.manifest.json",
|
||||
"posttest": "pnpm lint",
|
||||
"prepack": "pnpm build && oclif manifest"
|
||||
"analyzer": "node -r ts-node/register ./src/commands/analyzer/index.ts"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
|
|
|
@ -0,0 +1,91 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { Command } from '@commander-js/extra-typings';
|
||||
import { Logger } from 'cli-core/src/logger';
|
||||
import { join } from 'path';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { scanForChanges } from '../../lib/scan-changes';
|
||||
import {
|
||||
printDatabaseUpdates,
|
||||
printHookResults,
|
||||
printSchemaChange,
|
||||
printTemplateResults,
|
||||
} from '../../print';
|
||||
|
||||
const program = new Command()
|
||||
.command( 'lint' )
|
||||
.argument(
|
||||
'<compare>',
|
||||
'GitHub branch/tag/commit hash to compare against the base branch/tag/commit hash.'
|
||||
)
|
||||
.argument(
|
||||
'<sinceVersion>',
|
||||
'Specify the version used to determine which changes are included (version listed in @since code doc).'
|
||||
)
|
||||
.option(
|
||||
'-b, --base <base>',
|
||||
'GitHub base branch/tag/commit hash.',
|
||||
'trunk'
|
||||
)
|
||||
.option(
|
||||
'-s, --source <source>',
|
||||
'Git repo url or local path to a git repo.',
|
||||
join( process.cwd(), '../../' )
|
||||
)
|
||||
.option(
|
||||
'-o, --outputStyle <outputStyle>',
|
||||
'Output style for the results. Options: github, cli. Github output will use ::set-output to set the results as an output variable.',
|
||||
'cli'
|
||||
)
|
||||
.option(
|
||||
'-ss, --skipSchemaCheck',
|
||||
'Skip the schema check, enable this if you are not analyzing WooCommerce'
|
||||
)
|
||||
.action( async ( compare, sinceVersion, options ) => {
|
||||
const { skipSchemaCheck = false, source, base, outputStyle } = options;
|
||||
|
||||
const changes = await scanForChanges(
|
||||
compare,
|
||||
sinceVersion,
|
||||
skipSchemaCheck,
|
||||
source,
|
||||
base
|
||||
);
|
||||
|
||||
if ( changes.templates.size ) {
|
||||
printTemplateResults(
|
||||
Array.from( changes.templates.values() ),
|
||||
outputStyle,
|
||||
'TEMPLATES',
|
||||
Logger.notice
|
||||
);
|
||||
}
|
||||
|
||||
if ( changes.hooks.size ) {
|
||||
printHookResults(
|
||||
Array.from( changes.hooks.values() ),
|
||||
outputStyle,
|
||||
'HOOKS',
|
||||
Logger.notice
|
||||
);
|
||||
}
|
||||
|
||||
if ( changes.schema.filter( ( s ) => ! s.areEqual ).length ) {
|
||||
printSchemaChange(
|
||||
changes.schema,
|
||||
sinceVersion,
|
||||
outputStyle,
|
||||
Logger.notice
|
||||
);
|
||||
}
|
||||
|
||||
if ( changes.db ) {
|
||||
printDatabaseUpdates( changes.db, outputStyle, Logger.notice );
|
||||
}
|
||||
} );
|
||||
|
||||
program.parse( process.argv );
|
|
@ -0,0 +1,78 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { join } from 'path';
|
||||
import { readFile } from 'fs/promises';
|
||||
import simpleGit from 'simple-git';
|
||||
import { cloneRepo } from 'cli-core/src/git';
|
||||
import { Logger } from 'cli-core/src/logger';
|
||||
import { Command } from '@commander-js/extra-typings';
|
||||
|
||||
/**
|
||||
* Get plugin data
|
||||
*
|
||||
* @param {string} tmpRepoPath - Path to repo.
|
||||
* @param {string} pathToMainFile - Path to plugin's main PHP file.
|
||||
* @param {string} hashOrBranch - Hash or branch to checkout.
|
||||
* @return {Promise<string>} - Promise containing version as string.
|
||||
*/
|
||||
const getPluginData = async (
|
||||
tmpRepoPath: string,
|
||||
pathToMainFile: string,
|
||||
hashOrBranch: string
|
||||
): Promise< string | void > => {
|
||||
const git = simpleGit( { baseDir: tmpRepoPath } );
|
||||
await git.checkout( [ hashOrBranch ] );
|
||||
|
||||
const mainFile = join( tmpRepoPath, pathToMainFile );
|
||||
|
||||
Logger.startTask( `Getting version from ${ pathToMainFile }` );
|
||||
|
||||
const content = await readFile( mainFile, 'utf-8' );
|
||||
const rawVer = content.match( /^\s+\*\s+Version:\s+(.*)/m );
|
||||
|
||||
if ( rawVer && rawVer.length > 1 ) {
|
||||
const version = rawVer[ 1 ].replace( /\-.*/, '' );
|
||||
|
||||
Logger.endTask();
|
||||
|
||||
const [ major, minor ] = version.split( '.' );
|
||||
|
||||
return `${ major }.${ minor }.0`;
|
||||
}
|
||||
|
||||
Logger.error(
|
||||
'Failed to find plugin version! Make sure the file contains a version in the format `Version: ...`'
|
||||
);
|
||||
};
|
||||
|
||||
const program = new Command()
|
||||
.command( 'major-minor' )
|
||||
.argument( '<branch>', 'GitHub branch to use to determine version.' )
|
||||
.argument( '<pathToMainFile>', "Path to plugin's main PHP file." )
|
||||
.option(
|
||||
'-s, --source <source>',
|
||||
'Git repo url or local path to a git repo.',
|
||||
join( process.cwd(), '../../' )
|
||||
)
|
||||
.action( async ( branch, pathToMainFile, options ) => {
|
||||
const { source } = options;
|
||||
|
||||
Logger.startTask( `Making a temporary clone of '${ branch }'` );
|
||||
const tmpRepoPath = await cloneRepo( source );
|
||||
Logger.endTask();
|
||||
|
||||
const version = await getPluginData(
|
||||
tmpRepoPath,
|
||||
pathToMainFile,
|
||||
branch
|
||||
);
|
||||
|
||||
if ( version ) {
|
||||
Logger.notice( version );
|
||||
} else {
|
||||
Logger.error( 'Failed to get version' );
|
||||
}
|
||||
} );
|
||||
|
||||
program.parse( process.argv );
|
|
@ -1,445 +1,15 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { CliUx, Command, Flags } from '@oclif/core';
|
||||
import { join } from 'path';
|
||||
import { rmSync } from 'fs';
|
||||
import { program } from '@commander-js/extra-typings';
|
||||
import dotenv from 'dotenv';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import {
|
||||
printTemplateResults,
|
||||
printHookResults,
|
||||
printSchemaChange,
|
||||
printDatabaseUpdates,
|
||||
} from '../../print';
|
||||
import {
|
||||
getVersionRegex,
|
||||
getFilename,
|
||||
getPatches,
|
||||
getHookName,
|
||||
areSchemasEqual,
|
||||
getHookDescription,
|
||||
getHookChangeType,
|
||||
generateJSONFile,
|
||||
} from '../../utils';
|
||||
import { cloneRepo, generateDiff, generateSchemaDiff } from '../../git';
|
||||
import { execSync } from 'child_process';
|
||||
import { OutputFlags } from '@oclif/core/lib/interfaces';
|
||||
dotenv.config();
|
||||
|
||||
/**
|
||||
* Analyzer class
|
||||
*/
|
||||
export default class Analyzer extends Command {
|
||||
/**
|
||||
* CLI description
|
||||
*/
|
||||
static description = 'Analyze code changes in WooCommerce Monorepo.';
|
||||
program
|
||||
.name( 'analyzer' )
|
||||
.version( '0.0.1' )
|
||||
.command( 'lint', 'Lint changes', { isDefault: true } )
|
||||
.command( 'major-minor', 'Determine major/minor version of a plugin' );
|
||||
|
||||
/**
|
||||
* CLI arguments
|
||||
*/
|
||||
static args = [
|
||||
{
|
||||
name: 'compare',
|
||||
description:
|
||||
'GitHub branch or commit hash to compare against the base branch/commit.',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'sinceVersion',
|
||||
description:
|
||||
'Specify the version used to determine which changes are included (version listed in @since code doc).',
|
||||
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: 'Git repo url or local path to a git repo.',
|
||||
default: process.cwd(),
|
||||
} ),
|
||||
file: Flags.string( {
|
||||
char: 'f',
|
||||
description: 'Filename for change description JSON.',
|
||||
default: 'changes.json',
|
||||
} ),
|
||||
plugin: Flags.string( {
|
||||
char: 'p',
|
||||
description: 'Plugin to check for',
|
||||
options: [ 'core', 'admin', 'beta' ],
|
||||
default: 'core',
|
||||
} ),
|
||||
'is-woocommerce': Flags.boolean( {
|
||||
char: 'w',
|
||||
description:
|
||||
'Analyzing WooCommerce? (Will scan for DB schema changes).',
|
||||
default: true,
|
||||
} ),
|
||||
};
|
||||
|
||||
/**
|
||||
* This method is called to execute the command
|
||||
*/
|
||||
async run(): Promise< void > {
|
||||
const { args, flags } = await this.parse( Analyzer );
|
||||
|
||||
const { compare, sinceVersion } = args;
|
||||
const { base } = flags;
|
||||
|
||||
CliUx.ux.action.start(
|
||||
`Making a temporary clone of '${ flags.source }'`
|
||||
);
|
||||
const tmpRepoPath = await cloneRepo( flags.source );
|
||||
CliUx.ux.action.stop();
|
||||
|
||||
CliUx.ux.action.start(
|
||||
`Comparing '${ flags.base }' with '${ args.compare }'`
|
||||
);
|
||||
const diff = await generateDiff(
|
||||
tmpRepoPath,
|
||||
flags.base,
|
||||
compare,
|
||||
this.error
|
||||
);
|
||||
CliUx.ux.action.stop();
|
||||
|
||||
// Run schema diffs only in the monorepo.
|
||||
if ( flags[ 'is-woocommerce' ] ) {
|
||||
const pluginPath = join( tmpRepoPath, 'plugins/woocommerce' );
|
||||
|
||||
const build = () => {
|
||||
CliUx.ux.action.start( 'Building WooCommerce' );
|
||||
// Note doing the minimal work to get a DB scan to work, avoiding full build for speed.
|
||||
execSync( 'composer install', { cwd: pluginPath, stdio: [] } );
|
||||
execSync(
|
||||
'pnpm run build:feature-config --filter=woocommerce',
|
||||
{
|
||||
cwd: pluginPath,
|
||||
}
|
||||
);
|
||||
|
||||
CliUx.ux.action.stop();
|
||||
};
|
||||
|
||||
CliUx.ux.action.start(
|
||||
`Comparing WooCommerce DB schemas of '${ base }' and '${ compare }'`
|
||||
);
|
||||
|
||||
const schemaDiff = await generateSchemaDiff(
|
||||
tmpRepoPath,
|
||||
compare,
|
||||
base,
|
||||
build,
|
||||
( e: string ): void => this.error( e )
|
||||
);
|
||||
|
||||
CliUx.ux.action.stop();
|
||||
|
||||
await this.scanChanges( diff, sinceVersion, flags, schemaDiff );
|
||||
} else {
|
||||
await this.scanChanges( diff, sinceVersion, flags );
|
||||
}
|
||||
|
||||
// Clean up the temporary repo.
|
||||
CliUx.ux.action.start( 'Cleaning up temporary files' );
|
||||
rmSync( tmpRepoPath, { force: true, recursive: true } );
|
||||
CliUx.ux.action.stop();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* @param {string} changesFileName Name of a file to output change information to.
|
||||
* @param {boolean} schemaEquality if schemas are equal between branches.
|
||||
*/
|
||||
private async scanChanges(
|
||||
content: string,
|
||||
version: string,
|
||||
flags: OutputFlags< typeof Analyzer[ 'flags' ] >,
|
||||
schemaDiff: {
|
||||
[ key: string ]: {
|
||||
description: string;
|
||||
base: string;
|
||||
compare: string;
|
||||
method: string;
|
||||
areEqual: boolean;
|
||||
};
|
||||
} | void
|
||||
) {
|
||||
const { output, file } = flags;
|
||||
|
||||
const templates = this.scanTemplates( content, version );
|
||||
const hooks = this.scanHooks( content, version, output );
|
||||
const databaseUpdates = this.scanDatabases( content );
|
||||
let schemaDiffResult = {};
|
||||
|
||||
CliUx.ux.action.start(
|
||||
`Generating a list of changes since ${ version }.`
|
||||
);
|
||||
|
||||
if ( templates.size ) {
|
||||
printTemplateResults(
|
||||
templates,
|
||||
output,
|
||||
'TEMPLATE CHANGES',
|
||||
( s: string ): void => this.log( s )
|
||||
);
|
||||
} else {
|
||||
this.log( 'No template changes found' );
|
||||
}
|
||||
|
||||
if ( hooks.size ) {
|
||||
printHookResults( hooks, output, 'HOOKS', ( s: string ): void =>
|
||||
this.log( s )
|
||||
);
|
||||
} else {
|
||||
this.log( 'No new hooks found' );
|
||||
}
|
||||
|
||||
if ( ! areSchemasEqual( schemaDiff ) ) {
|
||||
schemaDiffResult = printSchemaChange(
|
||||
schemaDiff,
|
||||
version,
|
||||
output,
|
||||
( s: string ): void => this.log( s )
|
||||
);
|
||||
} else {
|
||||
this.log( 'No new schema changes found' );
|
||||
}
|
||||
|
||||
if ( databaseUpdates ) {
|
||||
printDatabaseUpdates(
|
||||
databaseUpdates,
|
||||
output,
|
||||
( s: string ): void => this.log( s )
|
||||
);
|
||||
} else {
|
||||
this.log( 'No database updates found' );
|
||||
}
|
||||
|
||||
await generateJSONFile( join( process.cwd(), file ), {
|
||||
templates: Object.fromEntries( templates.entries() ),
|
||||
hooks: Object.fromEntries( hooks.entries() ),
|
||||
db: databaseUpdates || {},
|
||||
schema: schemaDiffResult || {},
|
||||
} );
|
||||
|
||||
CliUx.ux.action.stop();
|
||||
}
|
||||
/**
|
||||
* Scan patches for changes in the database
|
||||
*
|
||||
* @param {string} content Patch content.
|
||||
* @param {string} version Current product version.
|
||||
* @param {string} output Output style.
|
||||
* @return {object|null}
|
||||
*/
|
||||
private scanDatabases(
|
||||
content: string
|
||||
): { updateFunctionName: string; updateFunctionVersion: string } | null {
|
||||
CliUx.ux.action.start( 'Scanning database changes' );
|
||||
const matchPatches = /^a\/(.+).php/g;
|
||||
const patches = getPatches( content, matchPatches );
|
||||
const databaseUpdatePatch = patches.find( ( patch ) => {
|
||||
const lines = patch.split( '\n' );
|
||||
const filepath = getFilename( lines[ 0 ] );
|
||||
return filepath.includes( 'class-wc-install.php' );
|
||||
} );
|
||||
|
||||
if ( ! databaseUpdatePatch ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const updateFunctionRegex =
|
||||
/\+{1,2}\s*'(\d.\d.\d)' => array\(\n\+{1,2}\s*'(.*)',\n\+{1,2}\s*\),/m;
|
||||
const match = databaseUpdatePatch.match( updateFunctionRegex );
|
||||
|
||||
if ( ! match ) {
|
||||
return null;
|
||||
}
|
||||
const updateFunctionVersion = match[ 1 ];
|
||||
const updateFunctionName = match[ 2 ];
|
||||
CliUx.ux.action.stop();
|
||||
return { updateFunctionName, updateFunctionVersion };
|
||||
}
|
||||
|
||||
/**
|
||||
* Scan patches for changes in templates
|
||||
*
|
||||
* @param {string} content Patch content.
|
||||
* @param {string} version Current product version.
|
||||
* @return {Promise<Map<string, string[]>>} Promise.
|
||||
*/
|
||||
private scanTemplates(
|
||||
content: string,
|
||||
version: string
|
||||
): Map< string, string[] > {
|
||||
CliUx.ux.action.start(
|
||||
`Scanning for template changes since ${ version }.`
|
||||
);
|
||||
|
||||
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 = 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 = 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 scanHooks(
|
||||
content: string,
|
||||
version: string,
|
||||
output: string
|
||||
): Map< string, Map< string, string[] > > {
|
||||
CliUx.ux.action.start( `Scanning for new hooks since ${ version }.` );
|
||||
|
||||
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 = getPatches( content, matchPatches );
|
||||
const verRegEx = 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 ];
|
||||
// Separate patches into bits beginning with a comment. If a bit does not have an action, disregard.
|
||||
const patchWithHook = patch.split( '/**' ).find( ( s ) => {
|
||||
return (
|
||||
s.includes( 'apply_filters' ) || s.includes( 'do_action' )
|
||||
);
|
||||
} );
|
||||
if ( ! patchWithHook ) {
|
||||
continue;
|
||||
}
|
||||
const results = patchWithHook.match( newRegEx );
|
||||
const hooksList: Map< string, string[] > = new Map<
|
||||
string,
|
||||
string[]
|
||||
>();
|
||||
|
||||
if ( ! results ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const lines = patch.split( '\n' );
|
||||
const filepath = 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 = getHookName( hookName[ 3 ] );
|
||||
|
||||
const description = getHookDescription( raw, name ) || '';
|
||||
|
||||
if ( ! description ) {
|
||||
this.error(
|
||||
`Hook ${ name } has no description. Please add a description.`,
|
||||
{ exit: false }
|
||||
);
|
||||
}
|
||||
|
||||
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 hookChangeType = getHookChangeType( raw );
|
||||
const title = `${ hookChangeType } ${ kind } found`;
|
||||
|
||||
if ( ! hookName[ 2 ].startsWith( '-' ) ) {
|
||||
hooksList.set( name, [
|
||||
'NOTICE',
|
||||
title,
|
||||
message,
|
||||
description,
|
||||
] );
|
||||
}
|
||||
}
|
||||
|
||||
report.set( filepath, hooksList );
|
||||
}
|
||||
|
||||
CliUx.ux.action.stop();
|
||||
return report;
|
||||
}
|
||||
}
|
||||
program.parse( process.argv );
|
||||
|
|
|
@ -1,110 +0,0 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { CliUx, Command, Flags } from '@oclif/core';
|
||||
import { join } from 'path';
|
||||
import { readFileSync, rmSync } from 'fs';
|
||||
import simpleGit from 'simple-git';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { cloneRepo } from '../../git';
|
||||
|
||||
/**
|
||||
* MajorMinor command class
|
||||
*/
|
||||
export default class MajorMinor extends Command {
|
||||
/**
|
||||
* CLI description
|
||||
*/
|
||||
static description = 'Determine major/minor version of a plugin';
|
||||
|
||||
/**
|
||||
* CLI arguments
|
||||
*/
|
||||
static args = [
|
||||
{
|
||||
name: 'branch',
|
||||
description: 'GitHub branch to use to determine version',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'pathToMainFile',
|
||||
description: "Path to plugin's main PHP file",
|
||||
required: true,
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* CLI flags.
|
||||
*/
|
||||
static flags = {
|
||||
source: Flags.string( {
|
||||
char: 's',
|
||||
description: 'Git repo url or local path to a git repo.',
|
||||
default: process.cwd(),
|
||||
} ),
|
||||
};
|
||||
|
||||
/**
|
||||
* This method is called to execute the command
|
||||
*/
|
||||
async run(): Promise< void > {
|
||||
const { args, flags } = await this.parse( MajorMinor );
|
||||
const { source } = flags;
|
||||
const { branch, pathToMainFile } = args;
|
||||
|
||||
CliUx.ux.action.start( `Making a temporary clone of '${ branch }'` );
|
||||
|
||||
const tmpRepoPath = await cloneRepo( source );
|
||||
const version = await this.getPluginData(
|
||||
tmpRepoPath,
|
||||
pathToMainFile,
|
||||
branch
|
||||
);
|
||||
|
||||
// Clean up the temporary repo.
|
||||
rmSync( tmpRepoPath, { force: true, recursive: true } );
|
||||
|
||||
this.log( version );
|
||||
CliUx.ux.action.stop();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get plugin data
|
||||
*
|
||||
* @param {string} tmpRepoPath - Path to repo.
|
||||
* @param {string} pathToMainFile - Path to plugin's main PHP file.
|
||||
* @param {string} hashOrBranch - Hash or branch to checkout.
|
||||
* @return {Promise<string>} - Promise containing version as string.
|
||||
*/
|
||||
private async getPluginData(
|
||||
tmpRepoPath: string,
|
||||
pathToMainFile: string,
|
||||
hashOrBranch: string
|
||||
): Promise< string > {
|
||||
const git = simpleGit( { baseDir: tmpRepoPath } );
|
||||
await git.checkout( [ hashOrBranch ] );
|
||||
|
||||
const mainFile = join( tmpRepoPath, pathToMainFile );
|
||||
|
||||
CliUx.ux.action.start( `Getting version from ${ pathToMainFile }` );
|
||||
|
||||
const content = readFileSync( mainFile ).toString();
|
||||
const rawVer = content.match( /^\s+\*\s+Version:\s+(.*)/m );
|
||||
|
||||
if ( ! rawVer ) {
|
||||
this.error(
|
||||
'Failed to find plugin version! Make sure the file contains a version in the format `Version: ...`'
|
||||
);
|
||||
}
|
||||
const version = rawVer[ 1 ].replace( /\-.*/, '' );
|
||||
|
||||
CliUx.ux.action.stop();
|
||||
|
||||
const [ major, minor ] = version.split( '.' );
|
||||
|
||||
return `${ major }.${ minor }.0`;
|
||||
}
|
||||
}
|
|
@ -1,207 +1,15 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { CliUx } from '@oclif/core';
|
||||
import { execSync } from 'child_process';
|
||||
import { join } from 'path';
|
||||
import { tmpdir } from 'os';
|
||||
import { mkdirSync } from 'fs';
|
||||
import { simpleGit } from 'simple-git';
|
||||
import { v4 } from 'uuid';
|
||||
import { mkdir, rm } from 'fs/promises';
|
||||
import { execAsync, startWPEnv, stopWPEnv } from 'cli-core/src/util';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { startWPEnv, stopWPEnv } from './utils';
|
||||
|
||||
/**
|
||||
* Check if a string is a valid url.
|
||||
*
|
||||
* @param {string} maybeURL - the URL string to check
|
||||
* @return {boolean} whether the string is a valid URL or not.
|
||||
*/
|
||||
const isUrl = ( maybeURL: string ) => {
|
||||
try {
|
||||
new URL( maybeURL );
|
||||
return true;
|
||||
} catch ( e ) {
|
||||
return false;
|
||||
}
|
||||
export type SchemaDump = {
|
||||
schema: string;
|
||||
OrdersTableDataStore: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Clone a git repository.
|
||||
*
|
||||
* @param {string} repoPath - the path (either URL or file path) to the repo to clone.
|
||||
* @return {Promise<string>} the path to the cloned repo.
|
||||
*/
|
||||
export const cloneRepo = async ( repoPath: string ) => {
|
||||
const folderPath = join( tmpdir(), 'code-analyzer-tmp', v4() );
|
||||
mkdirSync( folderPath, { recursive: true } );
|
||||
|
||||
const git = simpleGit( { baseDir: folderPath } );
|
||||
await git.clone( repoPath, folderPath );
|
||||
|
||||
// If this is a local clone then the simplest way to maintain remote settings is to copy git config across
|
||||
if ( ! isUrl( repoPath ) ) {
|
||||
execSync( `cp ${ repoPath }/.git/config ${ folderPath }/.git/config` );
|
||||
}
|
||||
|
||||
// Update the repo.
|
||||
await git.fetch();
|
||||
|
||||
return folderPath;
|
||||
};
|
||||
|
||||
/**
|
||||
* Do a minimal sparse checkout of a github repo.
|
||||
*
|
||||
* @param {string} githubRepoUrl - the URL to the repo to checkout.
|
||||
* @param {string} path - the path to checkout to.
|
||||
* @param {Array<string>} directories - the files or directories to checkout.
|
||||
* @return {Promise<string>} the path to the cloned repo.
|
||||
*/
|
||||
export const sparseCheckoutRepo = async (
|
||||
githubRepoUrl: string,
|
||||
path: string,
|
||||
directories: string[]
|
||||
) => {
|
||||
const folderPath = join( tmpdir(), path );
|
||||
|
||||
// clean up if it already exists.
|
||||
await rm( folderPath, { recursive: true, force: true } );
|
||||
await mkdir( folderPath, { recursive: true } );
|
||||
|
||||
const git = simpleGit( { baseDir: folderPath } );
|
||||
|
||||
await git.clone( githubRepoUrl, folderPath );
|
||||
await git.raw( 'sparse-checkout', 'init', { '--cone': null } );
|
||||
await git.raw( 'sparse-checkout', 'set', directories.join( ' ' ) );
|
||||
|
||||
return folderPath;
|
||||
};
|
||||
|
||||
/**
|
||||
* checkoutRef - checkout a ref in a git repo.
|
||||
*
|
||||
* @param {string} pathToRepo - the path to the repo to checkout a ref from.
|
||||
* @param {string} ref - the ref to checkout.
|
||||
* @return {Response<string>} - the simple-git response.
|
||||
*/
|
||||
export const checkoutRef = ( pathToRepo: string, ref: string ) => {
|
||||
const git = simpleGit( { baseDir: pathToRepo } );
|
||||
return git.checkout( ref );
|
||||
};
|
||||
|
||||
/**
|
||||
* Do a git diff of 2 commit hashes (or branches)
|
||||
*
|
||||
* @param {string} baseDir - baseDir that the repo is in
|
||||
* @param {string} hashA - either a git commit hash or a git branch
|
||||
* @param {string} hashB - either a git commit hash or a git branch
|
||||
* @return {Promise<string>} - diff of the changes between the 2 hashes
|
||||
*/
|
||||
export const diffHashes = ( baseDir: string, hashA: string, hashB: string ) => {
|
||||
const git = simpleGit( { baseDir } );
|
||||
return git.diff( [ `${ hashA }..${ hashB }` ] );
|
||||
};
|
||||
|
||||
/**
|
||||
* Determines if a string is a commit hash or not.
|
||||
*
|
||||
* @param {string} ref - the ref to check
|
||||
* @return {boolean} whether the ref is a commit hash or not.
|
||||
*/
|
||||
const refIsHash = ( ref: string ) => {
|
||||
return /^[0-9a-f]{7,40}$/i.test( ref );
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the commit hash for a ref (either branch or commit hash). If a validly
|
||||
* formed hash is provided it is returned unmodified.
|
||||
*
|
||||
* @param {string} baseDir - the dir of the git repo to get the hash from.
|
||||
* @param {string} ref - Either a commit hash or a branch name.
|
||||
* @return {string} - the commit hash of the ref.
|
||||
*/
|
||||
export const getCommitHash = async ( baseDir: string, ref: string ) => {
|
||||
const isHash = refIsHash( ref );
|
||||
|
||||
// check if its in history, if its not an error will be thrown
|
||||
try {
|
||||
await simpleGit( { baseDir } ).show( ref );
|
||||
} catch ( e ) {
|
||||
throw new Error(
|
||||
`${ ref } is not a valid commit hash or branch name that exists in git history`
|
||||
);
|
||||
}
|
||||
|
||||
// If its not a hash we assume its a branch
|
||||
if ( ! isHash ) {
|
||||
return simpleGit( { baseDir } ).revparse( [ ref ] );
|
||||
}
|
||||
|
||||
// Its a hash already
|
||||
return ref;
|
||||
};
|
||||
|
||||
/**
|
||||
* generateDiff generates a diff for a given repo and 2 hashes or branch names.
|
||||
*
|
||||
* @param {string} tmpRepoPath - filepath to the repo to generate a diff from.
|
||||
* @param {string} hashA - commit hash or branch name.
|
||||
* @param {string} hashB - commit hash or branch name.
|
||||
* @param {Function} onError - the handler to call when an error occurs.
|
||||
*/
|
||||
export const generateDiff = async (
|
||||
tmpRepoPath: string,
|
||||
hashA: string,
|
||||
hashB: string,
|
||||
onError: ( error: string ) => void
|
||||
) => {
|
||||
try {
|
||||
const git = simpleGit( { baseDir: tmpRepoPath } );
|
||||
|
||||
const validBranches = [ hashA, hashB ].filter(
|
||||
( hash ) => ! refIsHash( hash )
|
||||
);
|
||||
|
||||
// checking out any branches will automatically track remote branches.
|
||||
for ( const validBranch of validBranches ) {
|
||||
// Note you can't do checkouts in parallel otherwise the git binary will crash
|
||||
await git.checkout( [ validBranch ] );
|
||||
}
|
||||
|
||||
// turn both hashes into commit hashes if they are not already.
|
||||
const commitHashA = await getCommitHash( tmpRepoPath, hashA );
|
||||
const commitHashB = await getCommitHash( tmpRepoPath, hashB );
|
||||
|
||||
const isRepo = await simpleGit( {
|
||||
baseDir: tmpRepoPath,
|
||||
} ).checkIsRepo();
|
||||
|
||||
if ( ! isRepo ) {
|
||||
throw new Error( 'Not a git repository' );
|
||||
}
|
||||
|
||||
const diff = await diffHashes( tmpRepoPath, commitHashA, commitHashB );
|
||||
|
||||
return diff;
|
||||
} catch ( e ) {
|
||||
if ( e instanceof Error ) {
|
||||
onError(
|
||||
`Unable to create diff. Check that git repo, base hash, and compare hash all exist.\n Error: ${ e.message }`
|
||||
);
|
||||
} else {
|
||||
onError(
|
||||
'Unable to create diff. Check that git repo, base hash, and compare hash all exist.'
|
||||
);
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
};
|
||||
/**
|
||||
* Get all schema strings found in WooCommerce.
|
||||
*
|
||||
|
@ -212,17 +20,14 @@ export const generateDiff = async (
|
|||
export const getSchema = async (
|
||||
tmpRepoPath: string,
|
||||
error: ( s: string ) => void
|
||||
): Promise< {
|
||||
schema: string;
|
||||
OrdersTableDataStore: string;
|
||||
} | void > => {
|
||||
): Promise< SchemaDump | void > => {
|
||||
try {
|
||||
const pluginPath = join( tmpRepoPath, 'plugins/woocommerce' );
|
||||
const getSchemaPath =
|
||||
'wp-content/plugins/woocommerce/bin/wc-get-schema.php';
|
||||
|
||||
// Get the WooCommerce schema from wp cli
|
||||
const schema = execSync(
|
||||
const schemaOutput = await execAsync(
|
||||
`wp-env run cli "wp eval-file '${ getSchemaPath }'"`,
|
||||
{
|
||||
cwd: pluginPath,
|
||||
|
@ -231,7 +36,7 @@ export const getSchema = async (
|
|||
);
|
||||
|
||||
// Get the OrdersTableDataStore schema.
|
||||
const OrdersTableDataStore = execSync(
|
||||
const ordersTableOutput = await execAsync(
|
||||
'wp-env run cli "wp eval \'echo (new Automattic\\WooCommerce\\Internal\\DataStores\\Orders\\OrdersTableDataStore)->get_database_schema();\'"',
|
||||
{
|
||||
cwd: pluginPath,
|
||||
|
@ -240,8 +45,8 @@ export const getSchema = async (
|
|||
);
|
||||
|
||||
return {
|
||||
schema,
|
||||
OrdersTableDataStore,
|
||||
schema: schemaOutput.stdout,
|
||||
OrdersTableDataStore: ordersTableOutput.stdout,
|
||||
};
|
||||
} catch ( e ) {
|
||||
if ( e instanceof Error ) {
|
||||
|
@ -250,41 +55,40 @@ export const getSchema = async (
|
|||
}
|
||||
};
|
||||
|
||||
export type SchemaDiff = {
|
||||
name: string;
|
||||
description: string;
|
||||
base: string;
|
||||
compare: string;
|
||||
method: string;
|
||||
areEqual: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Generate a schema for each branch being compared.
|
||||
*
|
||||
* @param {string} tmpRepoPath Path to repository used to generate schema diff.
|
||||
* @param {string} compare Branch/commit hash to compare against the base.
|
||||
* @param {string} base Base branch/commit hash.
|
||||
* @param build Build to perform between checkouts.
|
||||
* @param {Function} build Build to perform between checkouts.
|
||||
* @param {Function} error error print method.
|
||||
* @return {Object|void} diff object.
|
||||
* @return {Promise<SchemaDiff[]|null>} diff object.
|
||||
*/
|
||||
export const generateSchemaDiff = async (
|
||||
tmpRepoPath: string,
|
||||
compare: string,
|
||||
base: string,
|
||||
build: () => void,
|
||||
build: () => Promise< void > | void,
|
||||
error: ( s: string ) => void
|
||||
): Promise< {
|
||||
[ key: string ]: {
|
||||
description: string;
|
||||
base: string;
|
||||
compare: string;
|
||||
method: string;
|
||||
areEqual: boolean;
|
||||
};
|
||||
} | void > => {
|
||||
): Promise< SchemaDiff[] | null > => {
|
||||
const git = simpleGit( { baseDir: tmpRepoPath } );
|
||||
|
||||
// Be sure the wp-env engine is started.
|
||||
await startWPEnv( tmpRepoPath, error );
|
||||
|
||||
CliUx.ux.action.start( `Gathering schema from ${ base }` );
|
||||
|
||||
// Force checkout because sometimes a build will generate a lockfile change.
|
||||
await git.checkout( base, [ '--force' ] );
|
||||
build();
|
||||
await build();
|
||||
const baseSchema = await getSchema(
|
||||
tmpRepoPath,
|
||||
( errorMessage: string ) => {
|
||||
|
@ -293,13 +97,10 @@ export const generateSchemaDiff = async (
|
|||
);
|
||||
}
|
||||
);
|
||||
CliUx.ux.action.stop();
|
||||
|
||||
CliUx.ux.action.start( `Gathering schema from ${ compare }` );
|
||||
|
||||
// Force checkout because sometimes a build will generate a lockfile change.
|
||||
await git.checkout( compare, [ '--force' ] );
|
||||
build();
|
||||
await build();
|
||||
const compareSchema = await getSchema(
|
||||
tmpRepoPath,
|
||||
( errorMessage: string ) => {
|
||||
|
@ -308,22 +109,24 @@ export const generateSchemaDiff = async (
|
|||
);
|
||||
}
|
||||
);
|
||||
CliUx.ux.action.stop();
|
||||
|
||||
stopWPEnv( tmpRepoPath, error );
|
||||
|
||||
if ( ! baseSchema || ! compareSchema ) {
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
schema: {
|
||||
|
||||
return [
|
||||
{
|
||||
name: 'schema',
|
||||
description: 'WooCommerce Base Schema',
|
||||
base: baseSchema.schema,
|
||||
compare: compareSchema.schema,
|
||||
method: 'WC_Install->get_schema',
|
||||
areEqual: baseSchema.schema === compareSchema.schema,
|
||||
},
|
||||
OrdersTableDataStore: {
|
||||
{
|
||||
name: 'OrdersTableDataStore',
|
||||
description: 'OrdersTableDataStore Schema',
|
||||
base: baseSchema.OrdersTableDataStore,
|
||||
compare: compareSchema.OrdersTableDataStore,
|
||||
|
@ -332,5 +135,5 @@ export const generateSchemaDiff = async (
|
|||
baseSchema.OrdersTableDataStore ===
|
||||
compareSchema.OrdersTableDataStore,
|
||||
},
|
||||
};
|
||||
];
|
||||
};
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { getFilename, getPatches } from 'cli-core/src/git';
|
||||
|
||||
export const scanForDBChanges = ( content: string ) => {
|
||||
const matchPatches = /^a\/(.+).php/g;
|
||||
const patches = getPatches( content, matchPatches );
|
||||
const databaseUpdatePatch = patches.find( ( patch ) => {
|
||||
const lines = patch.split( '\n' );
|
||||
const filepath = getFilename( lines[ 0 ] );
|
||||
return filepath.includes( 'class-wc-install.php' );
|
||||
} );
|
||||
|
||||
if ( ! databaseUpdatePatch ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const updateFunctionRegex =
|
||||
/\+{1,2}\s*'(\d.\d.\d)' => array\(\n\+{1,2}\s*'(.*)',\n\+{1,2}\s*\),/m;
|
||||
const match = databaseUpdatePatch.match( updateFunctionRegex );
|
||||
|
||||
if ( ! match ) {
|
||||
return null;
|
||||
}
|
||||
const updateFunctionVersion = match[ 1 ];
|
||||
const updateFunctionName = match[ 2 ];
|
||||
|
||||
return { updateFunctionName, updateFunctionVersion };
|
||||
};
|
|
@ -0,0 +1,89 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { getFilename, getPatches } from 'cli-core/src/util';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import {
|
||||
getHookChangeType,
|
||||
getHookDescription,
|
||||
getHookName,
|
||||
getVersionRegex,
|
||||
} from '../utils';
|
||||
|
||||
export type HookChangeDescription = {
|
||||
filePath: string;
|
||||
name: string;
|
||||
description: string;
|
||||
hookType: string;
|
||||
changeType: 'new' | 'updated';
|
||||
version: string;
|
||||
};
|
||||
|
||||
export const scanForHookChanges = ( content: string, version: string ) => {
|
||||
const changes: Map< string, HookChangeDescription > = new Map();
|
||||
|
||||
if ( ! content.match( /diff --git a\/(.+).php/g ) ) {
|
||||
return changes;
|
||||
}
|
||||
|
||||
const matchPatches = /^a\/(.+).php/g;
|
||||
const patches = getPatches( content, matchPatches );
|
||||
const verRegEx = 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 ];
|
||||
|
||||
// Separate patches into bits beginning with a comment. If a bit does not have an action, disregard.
|
||||
const patchWithHook = patch.split( '/**' ).find( ( s ) => {
|
||||
return s.includes( 'apply_filters' ) || s.includes( 'do_action' );
|
||||
} );
|
||||
if ( ! patchWithHook ) {
|
||||
continue;
|
||||
}
|
||||
const results = patchWithHook.match( newRegEx );
|
||||
|
||||
if ( ! results ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const lines = patch.split( '\n' );
|
||||
const filePath = 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 = getHookName( hookName[ 3 ] );
|
||||
const description = getHookDescription( raw, name ) || '';
|
||||
|
||||
const hookType =
|
||||
hookName[ 2 ] === 'do_action' ? 'action' : 'filter';
|
||||
|
||||
const changeType = getHookChangeType( raw );
|
||||
|
||||
if ( ! hookName[ 2 ].startsWith( '-' ) ) {
|
||||
changes.set( filePath, {
|
||||
filePath,
|
||||
name,
|
||||
hookType,
|
||||
description,
|
||||
changeType,
|
||||
version,
|
||||
} );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return changes;
|
||||
};
|
|
@ -0,0 +1,88 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { Logger } from 'cli-core/src/logger';
|
||||
import { join } from 'path';
|
||||
import { cloneRepo, generateDiff } from 'cli-core/src/git';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { execAsync } from '../utils';
|
||||
import { scanForDBChanges } from './db-changes';
|
||||
import { scanForHookChanges } from './hook-changes';
|
||||
import { scanForTemplateChanges } from './template-changes';
|
||||
import { SchemaDiff, generateSchemaDiff } from '../git';
|
||||
|
||||
export const scanForChanges = async (
|
||||
compareVersion: string,
|
||||
sinceVersion: string,
|
||||
skipSchemaCheck: boolean,
|
||||
source: string,
|
||||
base: string
|
||||
) => {
|
||||
Logger.startTask( `Making temporary clone of ${ source }...` );
|
||||
const tmpRepoPath = await cloneRepo( source );
|
||||
Logger.endTask();
|
||||
|
||||
Logger.notice(
|
||||
`Temporary clone of ${ source } created at ${ tmpRepoPath }`
|
||||
);
|
||||
|
||||
const diff = await generateDiff(
|
||||
tmpRepoPath,
|
||||
base,
|
||||
compareVersion,
|
||||
Logger.error
|
||||
);
|
||||
|
||||
const pluginPath = join( tmpRepoPath, 'plugins/woocommerce' );
|
||||
|
||||
Logger.startTask( 'Detecting hook changes...' );
|
||||
const hookChanges = scanForHookChanges( diff, sinceVersion );
|
||||
Logger.endTask();
|
||||
|
||||
Logger.startTask( 'Detecting template changes...' );
|
||||
const templateChanges = scanForTemplateChanges( diff, sinceVersion );
|
||||
Logger.endTask();
|
||||
|
||||
Logger.startTask( 'Detecting DB changes...' );
|
||||
const dbChanges = scanForDBChanges( diff );
|
||||
Logger.endTask();
|
||||
|
||||
let schemaChanges: SchemaDiff[] = [];
|
||||
|
||||
if ( ! skipSchemaCheck ) {
|
||||
const build = async () => {
|
||||
// Note doing the minimal work to get a DB scan to work, avoiding full build for speed.
|
||||
await execAsync( 'composer install', { cwd: pluginPath } );
|
||||
await execAsync(
|
||||
'pnpm run build:feature-config --filter=woocommerce',
|
||||
{
|
||||
cwd: pluginPath,
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
Logger.startTask( 'Generating schema diff...' );
|
||||
|
||||
const schemaDiff = await generateSchemaDiff(
|
||||
tmpRepoPath,
|
||||
compareVersion,
|
||||
base,
|
||||
build,
|
||||
Logger.error
|
||||
);
|
||||
|
||||
schemaChanges = schemaDiff || [];
|
||||
|
||||
Logger.endTask();
|
||||
}
|
||||
|
||||
return {
|
||||
hooks: hookChanges,
|
||||
templates: templateChanges,
|
||||
schema: schemaChanges,
|
||||
db: dbChanges,
|
||||
};
|
||||
};
|
|
@ -0,0 +1,49 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { getFilename, getPatches } from 'cli-core/src/git';
|
||||
|
||||
export type TemplateChangeDescription = {
|
||||
filePath: string;
|
||||
code: string;
|
||||
// We could probably move message out into linter later
|
||||
message: string;
|
||||
};
|
||||
|
||||
export const scanForTemplateChanges = ( content: string, version: string ) => {
|
||||
const changes: Map< string, TemplateChangeDescription > = new Map();
|
||||
|
||||
if ( ! content.match( /diff --git a\/(.+)\/templates\/(.+)\.php/g ) ) {
|
||||
return changes;
|
||||
}
|
||||
|
||||
const matchPatches = /^a\/(.+)\/templates\/(.+)/g;
|
||||
const patches = 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 = 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';
|
||||
}
|
||||
}
|
||||
|
||||
changes.set( filePath, { code, message, filePath } );
|
||||
}
|
||||
|
||||
return changes;
|
||||
};
|
|
@ -1,3 +1,10 @@
|
|||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { SchemaDiff } from './git';
|
||||
import { HookChangeDescription } from './lib/hook-changes';
|
||||
import { TemplateChangeDescription } from './lib/template-changes';
|
||||
|
||||
/**
|
||||
* Print template results
|
||||
*
|
||||
|
@ -7,32 +14,29 @@
|
|||
* @param {Function} log print method.
|
||||
*/
|
||||
export const printTemplateResults = (
|
||||
data: Map< string, string[] >,
|
||||
data: TemplateChangeDescription[],
|
||||
output: string,
|
||||
title: string,
|
||||
log: ( s: string ) => void
|
||||
): void => {
|
||||
//[code,title,message]
|
||||
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 ] }`;
|
||||
for ( const { filePath, code, message } of data ) {
|
||||
opt += `\\n* **file:** ${ filePath }`;
|
||||
opt += `\\n * ${ code.toUpperCase() }: ${ message }`;
|
||||
log(
|
||||
`::${ value[ 0 ] } file=${ key },line=1,title=${ value[ 1 ] }::${ value[ 2 ] }`
|
||||
`::${ code } file=${ filePath },line=1,title=${ title }::${ message }`
|
||||
);
|
||||
}
|
||||
|
||||
log( `::set-output name=templates::${ opt }` );
|
||||
} else {
|
||||
log( `\n## ${ title }:` );
|
||||
for ( const [ key, value ] of data ) {
|
||||
log( 'FILE: ' + key );
|
||||
for ( const { filePath, code, message } of data ) {
|
||||
log( 'FILE: ' + filePath );
|
||||
log( '---------------------------------------------------' );
|
||||
log(
|
||||
` ${ value[ 0 ].toUpperCase() } | ${ value[ 1 ] } | ${
|
||||
value[ 2 ]
|
||||
}`
|
||||
);
|
||||
log( ` ${ code.toUpperCase() } | ${ title } | ${ message }` );
|
||||
log( '---------------------------------------------------' );
|
||||
}
|
||||
}
|
||||
|
@ -43,52 +47,67 @@ export const printTemplateResults = (
|
|||
*
|
||||
* @param {Map} data Raw data.
|
||||
* @param {string} output Output style.
|
||||
* @param {string} title Section title.
|
||||
* @param {string} sectionTitle Section title.
|
||||
* @param {Function} log print method.
|
||||
*/
|
||||
export const printHookResults = (
|
||||
data: Map< string, Map< string, string[] > >,
|
||||
data: HookChangeDescription[],
|
||||
output: string,
|
||||
title: string,
|
||||
sectionTitle: string,
|
||||
log: ( s: string ) => void
|
||||
): void => {
|
||||
) => {
|
||||
// [
|
||||
// 'NOTICE',
|
||||
// title,
|
||||
// message,
|
||||
// description,
|
||||
// ]
|
||||
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 ] }: ${
|
||||
v[ 3 ]
|
||||
}`;
|
||||
for ( const {
|
||||
filePath,
|
||||
name,
|
||||
version,
|
||||
description,
|
||||
hookType,
|
||||
changeType,
|
||||
} of data ) {
|
||||
opt += `\\n* **file:** ${ filePath }`;
|
||||
|
||||
const cliMessage = `**${ name }** introduced in ${ version }`;
|
||||
const ghMessage = `\\'${ name }\\' introduced in ${ version }`;
|
||||
const message = output === 'github' ? ghMessage : cliMessage;
|
||||
const title = `${ changeType } ${ hookType } found`;
|
||||
|
||||
opt += `\\n * NOTICE - ${ message }: ${ description }`;
|
||||
log(
|
||||
`::${ v[ 0 ] } file=${ key },line=1,title=${ v[ 1 ] } - ${ k }::${ v[ 2 ] }`
|
||||
`::NOTICE file=${ filePath },line=1,title=${ title } - ${ name }::${ message }`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log( `::set-output name=wphooks::${ opt }` );
|
||||
} else {
|
||||
log( `\n## ${ title }:` );
|
||||
log( `\n## ${ sectionTitle }:` );
|
||||
log( '---------------------------------------------------' );
|
||||
for ( const [ key, value ] of data ) {
|
||||
if ( value.size ) {
|
||||
log( 'FILE: ' + key );
|
||||
for ( const {
|
||||
filePath,
|
||||
name,
|
||||
version,
|
||||
description,
|
||||
hookType,
|
||||
changeType,
|
||||
} of data ) {
|
||||
const cliMessage = `**${ name }** introduced in ${ version }`;
|
||||
const ghMessage = `\\'${ name }\\' introduced in ${ version }`;
|
||||
const message = output === 'github' ? ghMessage : cliMessage;
|
||||
const title = `${ changeType } ${ hookType } found`;
|
||||
|
||||
log( 'FILE: ' + filePath );
|
||||
log( '---------------------------------------------------' );
|
||||
log( `HOOK: ${ name }: ${ description }` );
|
||||
log( '---------------------------------------------------' );
|
||||
log( `NOTICE | ${ title } | ${ message }` );
|
||||
log( '---------------------------------------------------' );
|
||||
for ( const [ k, v ] of value ) {
|
||||
log( `HOOK: ${ k }: ${ v[ 3 ] }` );
|
||||
log(
|
||||
'---------------------------------------------------'
|
||||
);
|
||||
log(
|
||||
` ${ v[ 0 ].toUpperCase() } | ${ v[ 1 ] } | ${ v[ 2 ] }`
|
||||
);
|
||||
log(
|
||||
'---------------------------------------------------'
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -96,34 +115,22 @@ export const printHookResults = (
|
|||
/**
|
||||
* Print Schema change results.
|
||||
*
|
||||
* @param {Object} schemaDiff Schema diff object
|
||||
* @param {Object} schemaDiffs Schema diff object
|
||||
* @param {string} version Version change was introduced.
|
||||
* @param {string} output Output style.
|
||||
* @param {Function} log Print method.
|
||||
*/
|
||||
export const printSchemaChange = (
|
||||
schemaDiff: {
|
||||
[ key: string ]: {
|
||||
description: string;
|
||||
base: string;
|
||||
compare: string;
|
||||
method: string;
|
||||
areEqual: boolean;
|
||||
};
|
||||
} | void,
|
||||
schemaDiffs: SchemaDiff[],
|
||||
version: string,
|
||||
output: string,
|
||||
log: ( s: string ) => void
|
||||
): Record< string, string > => {
|
||||
const diff: Record< string, string > = {};
|
||||
if ( ! schemaDiff ) {
|
||||
return diff;
|
||||
}
|
||||
) => {
|
||||
if ( output === 'github' ) {
|
||||
let githubCommentContent = '\\n\\n### New schema changes:';
|
||||
Object.keys( schemaDiff ).forEach( ( key ) => {
|
||||
if ( ! schemaDiff[ key ].areEqual ) {
|
||||
githubCommentContent += `\\n* **Schema:** ${ schemaDiff[ key ].method } introduced in ${ version }`;
|
||||
schemaDiffs.forEach( ( schemaDiff ) => {
|
||||
if ( ! schemaDiff.areEqual ) {
|
||||
githubCommentContent += `\\n* **Schema:** ${ schemaDiff.method } introduced in ${ version }`;
|
||||
}
|
||||
} );
|
||||
|
||||
|
@ -131,18 +138,15 @@ export const printSchemaChange = (
|
|||
} else {
|
||||
log( '\n## SCHEMA CHANGES' );
|
||||
log( '---------------------------------------------------' );
|
||||
|
||||
Object.keys( schemaDiff ).forEach( ( key ) => {
|
||||
if ( ! schemaDiff[ key ].areEqual ) {
|
||||
schemaDiffs.forEach( ( schemaDiff ) => {
|
||||
if ( ! schemaDiff.areEqual ) {
|
||||
log(
|
||||
` NOTICE | Schema changes detected in ${ schemaDiff[ key ].method } as of ${ version }`
|
||||
` NOTICE | Schema changes detected in ${ schemaDiff.method } as of ${ version }`
|
||||
);
|
||||
log( '---------------------------------------------------' );
|
||||
diff[ key ] = schemaDiff[ key ].method;
|
||||
}
|
||||
} );
|
||||
}
|
||||
return diff;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
@ -1,10 +1,15 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { createServer, Server } from 'net';
|
||||
import { execSync } from 'child_process';
|
||||
import { join } from 'path';
|
||||
import { writeFile } from 'fs/promises';
|
||||
import { exec } from 'child_process';
|
||||
import { promisify } from 'util';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { SchemaDiff } from './git';
|
||||
|
||||
export const execAsync = promisify( exec );
|
||||
|
||||
/**
|
||||
* Format version string for regex.
|
||||
|
@ -22,39 +27,6 @@ export const getVersionRegex = ( rawVersion: string ): string => {
|
|||
return version;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get filename from patch
|
||||
*
|
||||
* @param {string} str String to extract filename from.
|
||||
* @return {string} formatted filename.
|
||||
*/
|
||||
export const getFilename = ( str: string ): string => {
|
||||
return str.replace( /^a(.*)\s.*/, '$1' );
|
||||
};
|
||||
|
||||
/**
|
||||
* Get patches
|
||||
*
|
||||
* @param {string} content Patch content.
|
||||
* @param {RegExp} regex Regex to find specific patches.
|
||||
* @return {string[]} Array of patches.
|
||||
*/
|
||||
export const getPatches = ( content: string, regex: RegExp ): 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 hook name.
|
||||
*
|
||||
|
@ -72,121 +44,15 @@ export const getHookName = ( name: string ): string => {
|
|||
/**
|
||||
* Determine if schema diff object contains schemas that are equal.
|
||||
*
|
||||
* @param {Object} schemaDiff
|
||||
* @param {Array<SchemaDiff>} schemaDiffs
|
||||
* @return {boolean|void} If the schema diff describes schemas that are equal.
|
||||
*/
|
||||
export const areSchemasEqual = (
|
||||
schemaDiff: {
|
||||
[ key: string ]: {
|
||||
description: string;
|
||||
base: string;
|
||||
compare: string;
|
||||
areEqual: boolean;
|
||||
};
|
||||
} | void
|
||||
): boolean => {
|
||||
if ( ! schemaDiff ) {
|
||||
return false;
|
||||
}
|
||||
return ! Object.keys( schemaDiff ).some(
|
||||
( d: string ) => schemaDiff[ d ].areEqual === false
|
||||
);
|
||||
export const areSchemasEqual = ( schemaDiffs: SchemaDiff[] ): boolean => {
|
||||
return ! schemaDiffs.some( ( s ) => ! s.areEqual );
|
||||
};
|
||||
|
||||
/**
|
||||
* Determine if the default port for wp-env is already taken. If so, see
|
||||
* https://developer.wordpress.org/block-editor/reference-guides/packages/packages-env/#2-check-the-port-number
|
||||
* for alternatives.
|
||||
*
|
||||
* @return {Promise<boolean>} if the port is being currently used.
|
||||
*/
|
||||
export const isWPEnvPortTaken = () => {
|
||||
return new Promise< boolean >( ( resolve, reject ) => {
|
||||
const test: Server = createServer()
|
||||
.once( 'error', ( err: { code: string } ) => {
|
||||
return err.code === 'EADDRINUSE'
|
||||
? resolve( true )
|
||||
: reject( err );
|
||||
} )
|
||||
.once( 'listening', () => {
|
||||
return test.once( 'close', () => resolve( false ) ).close();
|
||||
} )
|
||||
.listen( '8888' );
|
||||
} );
|
||||
};
|
||||
|
||||
/**
|
||||
* Start wp-env.
|
||||
*
|
||||
* @param {string} tmpRepoPath - path to the temporary repo to start wp-env from.
|
||||
* @param {Function} error - error print method.
|
||||
* @return {boolean} if starting the container succeeded.
|
||||
*/
|
||||
export const startWPEnv = async (
|
||||
tmpRepoPath: string,
|
||||
error: ( s: string ) => void
|
||||
) => {
|
||||
try {
|
||||
// Stop wp-env if its already running.
|
||||
execSync( 'wp-env stop', {
|
||||
cwd: join( tmpRepoPath, 'plugins/woocommerce' ),
|
||||
encoding: 'utf-8',
|
||||
} );
|
||||
} catch ( e ) {
|
||||
// If an error is produced here, it means wp-env is not initialized and therefore not running already.
|
||||
}
|
||||
|
||||
try {
|
||||
if ( await isWPEnvPortTaken() ) {
|
||||
throw new Error(
|
||||
'Unable to start wp-env. Make sure port 8888 is available or specify port number WP_ENV_PORT in .wp-env.override.json'
|
||||
);
|
||||
}
|
||||
|
||||
execSync( 'wp-env start', {
|
||||
cwd: join( tmpRepoPath, 'plugins/woocommerce' ),
|
||||
encoding: 'utf-8',
|
||||
} );
|
||||
return true;
|
||||
} catch ( e ) {
|
||||
let message = '';
|
||||
if ( e instanceof Error ) {
|
||||
message = e.message;
|
||||
error( message );
|
||||
}
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Stop wp-env.
|
||||
*
|
||||
* @param {string} tmpRepoPath - path to the temporary repo to stop wp-env from.
|
||||
* @param {Function} error - error print method.
|
||||
* @return {boolean} if stopping the container succeeded.
|
||||
*/
|
||||
export const stopWPEnv = (
|
||||
tmpRepoPath: string,
|
||||
error: ( s: string ) => void
|
||||
): boolean => {
|
||||
try {
|
||||
execSync( 'wp-env stop', {
|
||||
cwd: join( tmpRepoPath, 'plugins/woocommerce' ),
|
||||
encoding: 'utf-8',
|
||||
} );
|
||||
return true;
|
||||
} catch ( e ) {
|
||||
let message = '';
|
||||
if ( e instanceof Error ) {
|
||||
message = e.message;
|
||||
error( message );
|
||||
}
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Extrace hook description from a raw diff.
|
||||
* Extract hook description from a raw diff.
|
||||
*
|
||||
* @param {string} diff raw diff.
|
||||
* @param {string} name hook name.
|
||||
|
@ -229,23 +95,9 @@ export const getHookDescription = (
|
|||
* @param {string} diff raw diff.
|
||||
* @return {'Updated' | 'New'} change type.
|
||||
*/
|
||||
export const getHookChangeType = ( diff: string ): 'Updated' | 'New' => {
|
||||
export const getHookChangeType = ( diff: string ) => {
|
||||
const sincesRegex = /@since/g;
|
||||
const sinces = diff.match( sincesRegex ) || [];
|
||||
// If there is more than one 'since' in the diff, it means that line was updated meaning the hook already exists.
|
||||
return sinces.length > 1 ? 'Updated' : 'New';
|
||||
};
|
||||
|
||||
export const generateJSONFile = ( filePath: string, data: unknown ) => {
|
||||
const json = JSON.stringify(
|
||||
data,
|
||||
function replacer( key, value ) {
|
||||
if ( value instanceof Map ) {
|
||||
return Array.from( value.entries() );
|
||||
}
|
||||
return value;
|
||||
},
|
||||
2
|
||||
);
|
||||
return writeFile( filePath, json );
|
||||
return sinces.length > 1 ? 'updated' : 'new';
|
||||
};
|
||||
|
|
|
@ -1,15 +1,8 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"declaration": true,
|
||||
"importHelpers": true,
|
||||
"module": "commonjs",
|
||||
"outDir": "dist",
|
||||
"rootDir": "src",
|
||||
"strict": true,
|
||||
"target": "es2019",
|
||||
"typeRoots": [
|
||||
"./node_modules/@types"
|
||||
],
|
||||
"extends": "@tsconfig/node16/tsconfig.json",
|
||||
"ts-node": {
|
||||
"transpileOnly": true,
|
||||
"files": true,
|
||||
},
|
||||
"include": [
|
||||
"src/**/*"
|
||||
|
|
|
@ -11,8 +11,9 @@ don't have access to a wc.com auth token.
|
|||
1. Make sure `pnpm i` has been run in the monorepo.
|
||||
2. Make sure you have added a `.env` file with the env variables set. WCCOM_TOKEN is optional if you're using `--outputOnly`, but
|
||||
the `GITHUB_ACCESS_TOKEN` is required. If you need help generating a token see [the docs](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token). To silence all CLI output, set `LOGGER_LEVEL` to `"silent"`.
|
||||
3. Run the tool via the npm script, e.g. `pnpm run release "6.8.0" --outputOnly`
|
||||
4. For more help on individual options, run the help `pnpm run release --help`
|
||||
3. Note that the env file should live at the same path that you're running the command from.
|
||||
4. Run the tool via the npm script, e.g. `pnpm run release-post -- "6.8.0" --outputOnly`
|
||||
5. For more help on individual options, run the help `pnpm run release-post -- --help`
|
||||
### Publishing Draft Posts
|
||||
|
||||
This tool will publish draft posts to `https://developer.woocommerce.com` for you if you omit the `--outputOnly` flag. There is some minimal first time setup for this though:
|
||||
|
@ -32,6 +33,14 @@ app list and click "manage app".
|
|||
4. Take note of the `client secret` and the `client id`.
|
||||
5. In your `.env` file add the client secret to the `WPCOM_OAUTH_CLIENT_SECRET` variable and the client id to the `WPCOM_OAUTH_CLIENT_ID` variable.
|
||||
|
||||
|
||||
### Generating Just a Contributors List
|
||||
|
||||
If you don't have a final release yet you can generate an HTML contributors list that you can copy
|
||||
paste into a blank post.
|
||||
|
||||
To do that simply run `pnpm run release-post contributors -- "<currentVersion>" "<previousVersion>"`
|
||||
|
||||
### Advanced
|
||||
|
||||
If you can't run anything on your localhost port 3000 you may want to override the redirect uri for oauth.
|
|
@ -0,0 +1,18 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { program } from '@commander-js/extra-typings';
|
||||
import dotenv from 'dotenv';
|
||||
|
||||
dotenv.config();
|
||||
|
||||
program
|
||||
.name( 'release-post' )
|
||||
.version( '0.0.1' )
|
||||
.command( 'release', 'Generate release post', { isDefault: true } )
|
||||
.command(
|
||||
'contributors',
|
||||
'Generate a list of contributors for a release post'
|
||||
);
|
||||
|
||||
program.parse( process.argv );
|
|
@ -0,0 +1,52 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { Command } from '@commander-js/extra-typings';
|
||||
import { Logger } from 'cli-core/src/logger';
|
||||
import { writeFile } from 'fs/promises';
|
||||
import { tmpdir } from 'os';
|
||||
import { join } from 'path';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { generateContributors } from '../../lib/contributors';
|
||||
import { renderTemplate } from '../../lib/render-template';
|
||||
|
||||
// Define the contributors command
|
||||
const program = new Command()
|
||||
.command( 'contributors' )
|
||||
.description( 'CLI to automate generation of a release post.' )
|
||||
.argument(
|
||||
'<currentVersion>',
|
||||
'The version of the plugin to generate a post for, please use the tag version from Github.'
|
||||
)
|
||||
.argument(
|
||||
'--previousVersion <previousVersion>',
|
||||
'If you would like to compare against a version other than last minor you can provide a tag version from Github.'
|
||||
)
|
||||
.action( async ( currentVersion, previousVersion ) => {
|
||||
Logger.startTask( 'Generating contributors list...' );
|
||||
|
||||
const contributors = await generateContributors(
|
||||
currentVersion,
|
||||
previousVersion.toString()
|
||||
);
|
||||
|
||||
Logger.endTask();
|
||||
|
||||
const html = await renderTemplate( 'contributors.ejs', {
|
||||
contributors,
|
||||
} );
|
||||
|
||||
const tmpFile = join(
|
||||
tmpdir(),
|
||||
`contributors-${ currentVersion }.html`
|
||||
);
|
||||
|
||||
await writeFile( tmpFile, html );
|
||||
|
||||
Logger.notice( `Contributors HTML generated at ${ tmpFile }` );
|
||||
} );
|
||||
|
||||
program.parse( process.argv );
|
|
@ -1,30 +1,31 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import Analyzer from 'code-analyzer/src/commands/analyzer';
|
||||
import { scanForChanges } from 'code-analyzer/src/lib/scan-changes';
|
||||
import semver from 'semver';
|
||||
import { promises } from 'fs';
|
||||
import { writeFile } from 'fs/promises';
|
||||
import { tmpdir } from 'os';
|
||||
import { join } from 'path';
|
||||
import { Logger } from 'cli-core/src/logger';
|
||||
import { Command } from '@commander-js/extra-typings';
|
||||
import dotenv from 'dotenv';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { program } from '../program';
|
||||
import { renderTemplate } from '../lib/render-template';
|
||||
import { processChanges } from '../lib/process-changes';
|
||||
import { createWpComDraftPost } from '../lib/draft-post';
|
||||
import { generateContributors } from '../lib/contributors';
|
||||
import { Logger } from '../lib/logger';
|
||||
import { renderTemplate } from '../../lib/render-template';
|
||||
import { createWpComDraftPost } from '../../lib/draft-post';
|
||||
import { generateContributors } from '../../lib/contributors';
|
||||
|
||||
const DEVELOPER_WOOCOMMERCE_SITE_ID = '96396764';
|
||||
|
||||
const VERSION_VALIDATION_REGEX =
|
||||
/^([0-9]+)\.([0-9]+)\.([0-9]+)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+[0-9A-Za-z-]+)?$/;
|
||||
|
||||
dotenv.config();
|
||||
|
||||
// Define the release post command
|
||||
program
|
||||
const program = new Command()
|
||||
.command( 'release' )
|
||||
.description( 'CLI to automate generation of a release post.' )
|
||||
.argument(
|
||||
|
@ -36,7 +37,14 @@ program
|
|||
'--previousVersion <previousVersion>',
|
||||
'If you would like to compare against a version other than last minor you can provide a tag version from Github.'
|
||||
)
|
||||
.option(
|
||||
'--tags <tags>',
|
||||
'Comma separated list of tags to add to the post.',
|
||||
'Releases,WooCommerce Core'
|
||||
)
|
||||
.action( async ( currentVersion, options ) => {
|
||||
const tags = options.tags.split( ',' ).map( ( tag ) => tag.trim() );
|
||||
|
||||
const previousVersion = options.previousVersion
|
||||
? semver.parse( options.previousVersion )
|
||||
: semver.parse( currentVersion );
|
||||
|
@ -61,24 +69,17 @@ program
|
|||
);
|
||||
}
|
||||
|
||||
// generates a `changes.json` file in the current directory.
|
||||
await Analyzer.run( [
|
||||
const changes = await scanForChanges(
|
||||
currentVersion,
|
||||
currentVersion,
|
||||
'-s',
|
||||
false,
|
||||
'https://github.com/woocommerce/woocommerce.git',
|
||||
'-b',
|
||||
previousVersion.toString(),
|
||||
] );
|
||||
|
||||
const changes = JSON.parse(
|
||||
await promises.readFile(
|
||||
process.cwd() + '/changes.json',
|
||||
'utf8'
|
||||
)
|
||||
previousVersion.toString()
|
||||
);
|
||||
|
||||
const changeset = processChanges( changes );
|
||||
const schemaChanges = changes.schema.filter(
|
||||
( s ) => ! s.areEqual
|
||||
);
|
||||
|
||||
Logger.startTask( 'Finding contributors' );
|
||||
const title = `WooCommerce ${ currentVersion } Released`;
|
||||
|
@ -91,7 +92,10 @@ program
|
|||
const html = await renderTemplate( 'release.ejs', {
|
||||
contributors,
|
||||
title,
|
||||
changes: changeset,
|
||||
changes: {
|
||||
...changes,
|
||||
schema: schemaChanges,
|
||||
},
|
||||
displayVersion: currentVersion,
|
||||
} );
|
||||
|
||||
|
@ -113,7 +117,8 @@ program
|
|||
const { URL } = await createWpComDraftPost(
|
||||
DEVELOPER_WOOCOMMERCE_SITE_ID,
|
||||
title,
|
||||
html
|
||||
html,
|
||||
tags
|
||||
);
|
||||
|
||||
Logger.notice( `Published draft release post at ${ URL }` );
|
||||
|
@ -130,3 +135,5 @@ program
|
|||
);
|
||||
}
|
||||
} );
|
||||
|
||||
program.parse( process.argv );
|
|
@ -1,7 +1,7 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { checkoutRef, sparseCheckoutRepo } from 'code-analyzer/src/git';
|
||||
import { checkoutRef, sparseCheckoutRepo } from 'cli-core/src/git';
|
||||
import { readFile } from 'fs/promises';
|
||||
import { join } from 'path';
|
||||
import semver from 'semver';
|
|
@ -2,13 +2,13 @@
|
|||
* External dependencies
|
||||
*/
|
||||
import fetch from 'node-fetch';
|
||||
import { Logger } from 'cli-core/src/logger';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { getWordpressComAuthToken } from './oauth-helper';
|
||||
import { getEnvVar } from './environment';
|
||||
import { Logger } from './logger';
|
||||
|
||||
/**
|
||||
* Create a draft of a post on wordpress.com
|
||||
|
@ -21,7 +21,8 @@ import { Logger } from './logger';
|
|||
export const createWpComDraftPost = async (
|
||||
siteId: string,
|
||||
postTitle: string,
|
||||
postContent: string
|
||||
postContent: string,
|
||||
tags: string[]
|
||||
) => {
|
||||
const clientId = getEnvVar( 'WPCOM_OAUTH_CLIENT_ID', true );
|
||||
const clientSecret = getEnvVar( 'WPCOM_OAUTH_CLIENT_SECRET', true );
|
||||
|
@ -56,6 +57,7 @@ export const createWpComDraftPost = async (
|
|||
title: postTitle,
|
||||
content: postContent,
|
||||
status: 'draft',
|
||||
tags,
|
||||
} ),
|
||||
}
|
||||
);
|
|
@ -0,0 +1,17 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { Logger } from 'cli-core/src/logger';
|
||||
|
||||
|
||||
export const getEnvVar = ( varName: string, isRequired = false ) => {
|
||||
const value = process.env[ varName ];
|
||||
|
||||
if ( value === undefined && isRequired ) {
|
||||
Logger.error(
|
||||
`You need to provide a value for ${ varName } in your environment either via an environment variable or the .env file.`
|
||||
);
|
||||
}
|
||||
|
||||
return value || '';
|
||||
};
|
|
@ -4,9 +4,9 @@
|
|||
"description": "Automate release post generation for Wordpress plugins",
|
||||
"main": " ",
|
||||
"scripts": {
|
||||
"release": "ts-node ./index.ts release --"
|
||||
"release-post": "node -r ts-node/register ./commands/release-post/index.ts"
|
||||
},
|
||||
"author": "",
|
||||
"author": "Automattic",
|
||||
"license": "GPL-2.0-or-later",
|
||||
"devDependencies": {
|
||||
"@tsconfig/node16": "^1.0.3",
|
||||
|
@ -16,7 +16,7 @@
|
|||
"dependencies": {
|
||||
"@commander-js/extra-typings": "^0.1.0",
|
||||
"@octokit/rest": "^19.0.4",
|
||||
"chalk": "^4.1.2",
|
||||
"cli-core": "workspace:*",
|
||||
"code-analyzer": "workspace:*",
|
||||
"commander": "9.4.0",
|
||||
"dotenv": "^10.0.0",
|
||||
|
@ -26,7 +26,6 @@
|
|||
"lodash.shuffle": "^4.2.0",
|
||||
"node-fetch": "^2.6.7",
|
||||
"open": "^8.4.0",
|
||||
"ora": "^5.4.1",
|
||||
"semver": "^7.3.2",
|
||||
"ts-node": "^10.9.1"
|
||||
}
|
|
@ -15,11 +15,13 @@
|
|||
<th><strong>Class Name</strong></th>
|
||||
<th><strong>Code Ref</strong></th>
|
||||
</tr>
|
||||
<% changes.schema.forEach(({className, codeRef}) => { %>
|
||||
<% changes.schema.forEach(({areEqual, name, method}) => { %>
|
||||
<% if (!areEqual) { %>
|
||||
<tr>
|
||||
<td><%= className %></td>
|
||||
<td><%= codeRef %></td>
|
||||
<td><%= name %></td>
|
||||
<td><code><%= method %></code></td>
|
||||
</tr>
|
||||
<% } %>
|
||||
<% }) %>
|
||||
</tbody>
|
||||
</table>
|
||||
|
@ -27,7 +29,7 @@
|
|||
<!-- /wp:table -->
|
||||
<% } %>
|
||||
|
||||
<% if (changes.db.length) { %>
|
||||
<% if (changes.db) { %>
|
||||
<!-- wp:heading -->
|
||||
<h3 id="db-updates">DB Updates</h3>
|
||||
<!-- /wp:heading -->
|
||||
|
@ -41,11 +43,9 @@
|
|||
<strong>Function Name</strong>
|
||||
</th>
|
||||
</tr>
|
||||
<% changes.db.forEach(({functionName}) => { %>
|
||||
<tr>
|
||||
<td><%= functionName %></td>
|
||||
<td><code><%= changes.db.functionName %></code></td>
|
||||
</tr>
|
||||
<% }) %>
|
||||
</tbody>
|
||||
</table>
|
||||
</figure>
|
|
@ -2,7 +2,7 @@
|
|||
<h2 id="actions-and-filters">Actions and Filters</h2>
|
||||
<!-- /wp:heading -->
|
||||
|
||||
<% if (changes.hooks.length) { %>
|
||||
<% if (changes.hooks.size) { %>
|
||||
<!-- wp:table -->
|
||||
<figure class="wp-block-table">
|
||||
<table>
|
||||
|
@ -11,7 +11,7 @@
|
|||
<th><strong>Filter</strong></th>
|
||||
<th><strong>Description</strong></th>
|
||||
</tr>
|
||||
<% changes.hooks.forEach(({name, description}) => { %>
|
||||
<% changes.hooks.forEach(({ name, description }) => { %>
|
||||
<tr>
|
||||
<td><%= name %></td>
|
||||
<td><%= description %></td>
|
|
@ -2,7 +2,7 @@
|
|||
<h2 id="template-changes">Template Changes</h2>
|
||||
<!-- /wp:heading -->
|
||||
|
||||
<% if (changes.templates.length) { %>
|
||||
<% if (changes.templates.size) { %>
|
||||
<!-- wp:table -->
|
||||
<figure class="wp-block-table">
|
||||
<table>
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"extends": "@tsconfig/node16/tsconfig.json",
|
||||
"ts-node": {
|
||||
"transpileOnly": true,
|
||||
"files": true,
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue