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:
Sam Seay 2022-09-11 09:55:53 +12:00 committed by GitHub
parent 03b9032de8
commit 614d98ff60
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
53 changed files with 5116 additions and 4202 deletions

View File

@ -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 }}"

File diff suppressed because it is too large Load Diff

View File

@ -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'

4
tools/cli-core/README.md Normal file
View File

@ -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.

View File

@ -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"
}
}

232
tools/cli-core/src/git.ts Normal file
View File

@ -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 '';
}
};

View File

@ -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;
}
}
}

172
tools/cli-core/src/util.ts Normal file
View File

@ -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 );
};

View File

@ -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`.

View File

@ -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)

View File

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

View File

@ -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'))

View File

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

View File

@ -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"

View File

@ -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 );

View File

@ -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 );

View File

@ -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 );

View File

@ -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`;
}
}

View File

@ -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,
},
};
];
};

View File

@ -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 };
};

View File

@ -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;
};

View File

@ -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,
};
};

View File

@ -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;
};

View File

@ -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;
};
/**

View File

@ -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';
};

View File

@ -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/**/*"

View File

@ -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.

View File

@ -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 );

View File

@ -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 );

View File

@ -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 );

View File

@ -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';

View File

@ -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,
} ),
}
);

View File

@ -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 || '';
};

View File

@ -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"
}

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -0,0 +1,7 @@
{
"extends": "@tsconfig/node16/tsconfig.json",
"ts-node": {
"transpileOnly": true,
"files": true,
}
}