Code Freeze CLI: Add version bump (#38036)
This commit is contained in:
parent
ea58ae339a
commit
05452979df
|
@ -9,11 +9,13 @@ import { Command } from '@commander-js/extra-typings';
|
|||
import { verifyDayCommand } from './verify-day';
|
||||
import { milestoneCommand } from './milestone';
|
||||
import { branchCommand } from './branch';
|
||||
import { versionBumpCommand } from './version-bump';
|
||||
|
||||
const program = new Command( 'code-freeze' )
|
||||
.description( 'Code freeze utilities' )
|
||||
.addCommand( verifyDayCommand )
|
||||
.addCommand( milestoneCommand )
|
||||
.addCommand( branchCommand );
|
||||
.addCommand( branchCommand )
|
||||
.addCommand( versionBumpCommand );
|
||||
|
||||
export default program;
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { stripPrereleaseParameters } from './lib/validate';
|
||||
import {
|
||||
updatePluginFile,
|
||||
updateReadmeChangelog,
|
||||
updateJSON,
|
||||
updateClassPluginFile,
|
||||
} from './lib/update';
|
||||
|
||||
export const bumpFiles = async ( tmpRepoPath, version ) => {
|
||||
let nextVersion = version;
|
||||
|
||||
await updatePluginFile( tmpRepoPath, nextVersion );
|
||||
|
||||
// Any updated files besides the plugin file get a version stripped of prerelease parameters.
|
||||
nextVersion = stripPrereleaseParameters( nextVersion );
|
||||
|
||||
// Bumping the dev version means updating the readme's changelog.
|
||||
await updateReadmeChangelog( tmpRepoPath, nextVersion );
|
||||
|
||||
await updateJSON( 'composer', tmpRepoPath, nextVersion );
|
||||
await updateJSON( 'package', tmpRepoPath, nextVersion );
|
||||
await updateClassPluginFile( tmpRepoPath, nextVersion );
|
||||
};
|
|
@ -0,0 +1,120 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { Command } from '@commander-js/extra-typings';
|
||||
import simpleGit from 'simple-git';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { Logger } from '../../../core/logger';
|
||||
import { sparseCheckoutRepoShallow } from '../../../core/git';
|
||||
import { octokitWithAuth } from '../../../core/github/api';
|
||||
import { getEnvVar } from '../../../core/environment';
|
||||
import { getMajorMinor } from '../../../core/version';
|
||||
import { bumpFiles } from './bump';
|
||||
import { validateArgs } from './lib/validate';
|
||||
import { Options } from './types';
|
||||
|
||||
const genericErrorFunction = ( err ) => {
|
||||
if ( err.git ) {
|
||||
return err.git;
|
||||
}
|
||||
throw err;
|
||||
};
|
||||
|
||||
export const versionBumpCommand = new Command( 'version-bump' )
|
||||
.description( 'Bump versions ahead of new development cycle' )
|
||||
.requiredOption( '-v, --version <version>', 'Version to bump to' )
|
||||
.option(
|
||||
'-o --owner <owner>',
|
||||
'Repository owner. Default: woocommerce',
|
||||
'woocommerce'
|
||||
)
|
||||
.option(
|
||||
'-n --name <name>',
|
||||
'Repository name. Default: woocommerce',
|
||||
'woocommerce'
|
||||
)
|
||||
.option(
|
||||
'-b --base <base>',
|
||||
'Base branch to create the PR against. Default: trunk',
|
||||
'trunk'
|
||||
)
|
||||
.action( async ( options: Options ) => {
|
||||
const { owner, name, version, base } = options;
|
||||
Logger.startTask(
|
||||
`Making a temporary clone of '${ owner }/${ name }'`
|
||||
);
|
||||
const source = `github.com/${ owner }/${ name }`;
|
||||
const token = getEnvVar( 'GITHUB_TOKEN', true );
|
||||
const remote = `https://${ owner }:${ token }@${ source }`;
|
||||
const tmpRepoPath = await sparseCheckoutRepoShallow(
|
||||
remote,
|
||||
'woocommerce',
|
||||
[
|
||||
'plugins/woocommerce/includes/class-woocommerce.php',
|
||||
// All that's needed is the line above, but including these here for completeness.
|
||||
'plugins/woocommerce/composer.json',
|
||||
'plugins/woocommerce/package.json',
|
||||
'plugins/woocommerce/readme.txt',
|
||||
'plugins/woocommerce/woocommerce.php',
|
||||
]
|
||||
);
|
||||
Logger.endTask();
|
||||
|
||||
Logger.notice(
|
||||
`Temporary clone of '${ owner }/${ name }' created at ${ tmpRepoPath }`
|
||||
);
|
||||
|
||||
await validateArgs( tmpRepoPath, version );
|
||||
|
||||
const git = simpleGit( {
|
||||
baseDir: tmpRepoPath,
|
||||
config: [ 'core.hooksPath=/dev/null' ],
|
||||
} );
|
||||
const majorMinor = getMajorMinor( version );
|
||||
const branch = `prep/trunk-for-next-dev-cycle-${ majorMinor }`;
|
||||
const exists = await git.raw( 'ls-remote', 'origin', branch );
|
||||
|
||||
if ( exists.trim().length > 0 ) {
|
||||
Logger.error(
|
||||
`Branch ${ branch } already exists. Run \`git push <remote> --delete ${ branch }\` and rerun this command.`
|
||||
);
|
||||
}
|
||||
|
||||
await git.checkoutBranch( branch, base ).catch( genericErrorFunction );
|
||||
|
||||
Logger.notice( `Bumping versions in ${ owner }/${ name }` );
|
||||
bumpFiles( tmpRepoPath, version );
|
||||
|
||||
Logger.notice( 'Adding and committing changes' );
|
||||
await git.add( '.' ).catch( genericErrorFunction );
|
||||
await git
|
||||
.commit( `Prep trunk for ${ majorMinor } cycle` )
|
||||
.catch( genericErrorFunction );
|
||||
|
||||
Logger.notice( 'Pushing to Github' );
|
||||
await git.push( 'origin', branch ).catch( ( e ) => {
|
||||
Logger.error( e );
|
||||
} );
|
||||
|
||||
try {
|
||||
Logger.startTask( 'Creating a pull request' );
|
||||
const pr = await octokitWithAuth.request(
|
||||
'POST /repos/{owner}/{repo}/pulls',
|
||||
{
|
||||
owner,
|
||||
repo: name,
|
||||
title: `Prep trunk for ${ majorMinor } cycle`,
|
||||
body: `This PR updates the versions in trunk to ${ version } for next development cycle.`,
|
||||
head: branch,
|
||||
base,
|
||||
}
|
||||
);
|
||||
Logger.notice( `Pull request created: ${ pr.data.html_url }` );
|
||||
Logger.endTask();
|
||||
} catch ( e ) {
|
||||
Logger.error( e );
|
||||
}
|
||||
} );
|
|
@ -0,0 +1,118 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { readFile, writeFile } from 'fs/promises';
|
||||
import { existsSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { Logger } from '../../../../core/logger';
|
||||
|
||||
/**
|
||||
* Update plugin readme changelog.
|
||||
*
|
||||
* @param tmpRepoPath cloned repo path
|
||||
* @param nextVersion version to bump to
|
||||
*/
|
||||
export const updateReadmeChangelog = async (
|
||||
tmpRepoPath: string,
|
||||
nextVersion: string
|
||||
): Promise< void > => {
|
||||
const filePath = join( tmpRepoPath, 'plugins/woocommerce/readme.txt' );
|
||||
try {
|
||||
const readmeContents = await readFile( filePath, 'utf8' );
|
||||
|
||||
const updatedReadmeContents = readmeContents.replace(
|
||||
/= \d+\.\d+\.\d+ \d\d\d\d-XX-XX =\n/m,
|
||||
`= ${ nextVersion } ${ new Date().getFullYear() }-XX-XX =\n`
|
||||
);
|
||||
|
||||
await writeFile( filePath, updatedReadmeContents );
|
||||
} catch ( e ) {
|
||||
Logger.error( e );
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Update plugin class file.
|
||||
*
|
||||
* @param tmpRepoPath cloned repo path
|
||||
* @param nextVersion version to bump to
|
||||
*/
|
||||
export const updateClassPluginFile = async (
|
||||
tmpRepoPath: string,
|
||||
nextVersion: string
|
||||
): Promise< void > => {
|
||||
const filePath = join(
|
||||
tmpRepoPath,
|
||||
`plugins/woocommerce/includes/class-woocommerce.php`
|
||||
);
|
||||
|
||||
if ( ! existsSync( filePath ) ) {
|
||||
Logger.error( "File 'class-woocommerce.php' does not exist." );
|
||||
}
|
||||
|
||||
try {
|
||||
const classPluginFileContents = await readFile( filePath, 'utf8' );
|
||||
|
||||
const updatedClassPluginFileContents = classPluginFileContents.replace(
|
||||
/public \$version = '\d+\.\d+\.\d+';\n/m,
|
||||
`public $version = '${ nextVersion }';\n`
|
||||
);
|
||||
|
||||
await writeFile( filePath, updatedClassPluginFileContents );
|
||||
} catch ( e ) {
|
||||
Logger.error( e );
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Update plugin JSON files.
|
||||
*
|
||||
* @param {string} type plugin to update
|
||||
* @param {string} tmpRepoPath cloned repo path
|
||||
* @param {string} nextVersion version to bump to
|
||||
*/
|
||||
export const updateJSON = async (
|
||||
type: 'package' | 'composer',
|
||||
tmpRepoPath: string,
|
||||
nextVersion: string
|
||||
): Promise< void > => {
|
||||
const filePath = join( tmpRepoPath, `plugins/woocommerce/${ type }.json` );
|
||||
try {
|
||||
const composerJson = JSON.parse( await readFile( filePath, 'utf8' ) );
|
||||
composerJson.version = nextVersion;
|
||||
await writeFile(
|
||||
filePath,
|
||||
JSON.stringify( composerJson, null, '\t' ) + '\n'
|
||||
);
|
||||
} catch ( e ) {
|
||||
Logger.error( e );
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Update plugin main file.
|
||||
*
|
||||
* @param tmpRepoPath cloned repo path
|
||||
* @param nextVersion version to bump to
|
||||
*/
|
||||
export const updatePluginFile = async (
|
||||
tmpRepoPath: string,
|
||||
nextVersion: string
|
||||
): Promise< void > => {
|
||||
const filePath = join( tmpRepoPath, `plugins/woocommerce/woocommerce.php` );
|
||||
try {
|
||||
const pluginFileContents = await readFile( filePath, 'utf8' );
|
||||
|
||||
const updatedPluginFileContents = pluginFileContents.replace(
|
||||
/Version: \d+\.\d+\.\d+.*\n/m,
|
||||
`Version: ${ nextVersion }\n`
|
||||
);
|
||||
await writeFile( filePath, updatedPluginFileContents );
|
||||
} catch ( e ) {
|
||||
Logger.error( e );
|
||||
}
|
||||
};
|
|
@ -0,0 +1,84 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { valid, lt as versionLessThan, parse, prerelease } from 'semver';
|
||||
import { join } from 'path';
|
||||
import { readFile } from 'fs/promises';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { Logger } from '../../../../core/logger';
|
||||
/**
|
||||
* Get a plugin's current version.
|
||||
*
|
||||
* @param tmpRepoPath cloned repo path
|
||||
*/
|
||||
export const getCurrentVersion = async (
|
||||
tmpRepoPath: string
|
||||
): Promise< string | void > => {
|
||||
const filePath = join( tmpRepoPath, `plugins/woocommerce/composer.json` );
|
||||
try {
|
||||
const composerJSON = JSON.parse( await readFile( filePath, 'utf8' ) );
|
||||
return composerJSON.version;
|
||||
} catch ( e ) {
|
||||
Logger.error( e );
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* When given a prerelease version, return just the version.
|
||||
*
|
||||
* @param {string} prereleaseVersion version with prerelease params
|
||||
* @return {string} version
|
||||
*/
|
||||
export const stripPrereleaseParameters = (
|
||||
prereleaseVersion: string
|
||||
): string => {
|
||||
const parsedVersion = parse( prereleaseVersion );
|
||||
if ( parsedVersion ) {
|
||||
const { major, minor, patch } = parsedVersion;
|
||||
return `${ major }.${ minor }.${ patch }`;
|
||||
}
|
||||
return prereleaseVersion;
|
||||
};
|
||||
|
||||
/**
|
||||
* Validate inputs.
|
||||
*
|
||||
* @param plugin plugin
|
||||
* @param options options
|
||||
* @param options.version version
|
||||
*/
|
||||
export const validateArgs = async (
|
||||
tmpRepoPath: string,
|
||||
version: string
|
||||
): Promise< void > => {
|
||||
const nextVersion = version;
|
||||
|
||||
if ( ! valid( nextVersion ) ) {
|
||||
Logger.error(
|
||||
'Invalid version supplied, please pass in a semantically correct version.'
|
||||
);
|
||||
}
|
||||
|
||||
const prereleaseParameters = prerelease( nextVersion );
|
||||
const isDevVersionBump =
|
||||
prereleaseParameters && prereleaseParameters[ 0 ] === 'dev';
|
||||
|
||||
if ( ! isDevVersionBump ) {
|
||||
Logger.error(
|
||||
`Version ${ nextVersion } is not a development version bump. This tool is only intended to bump development versions for the preparation of the next development cycle.`
|
||||
);
|
||||
}
|
||||
|
||||
const currentVersion = await getCurrentVersion( tmpRepoPath );
|
||||
|
||||
if ( ! currentVersion ) {
|
||||
Logger.error( 'Unable to determine current version' );
|
||||
} else if ( versionLessThan( nextVersion, currentVersion ) ) {
|
||||
Logger.error(
|
||||
'The version supplied is less than the current version, please supply a valid version.'
|
||||
);
|
||||
}
|
||||
};
|
|
@ -0,0 +1,6 @@
|
|||
export type Options = {
|
||||
owner?: string;
|
||||
name?: string;
|
||||
version?: string;
|
||||
base?: string;
|
||||
};
|
|
@ -5,7 +5,7 @@ import { execSync } from 'child_process';
|
|||
import { join } from 'path';
|
||||
import { tmpdir } from 'os';
|
||||
import { mkdirSync } from 'fs';
|
||||
import { simpleGit } from 'simple-git';
|
||||
import { simpleGit, TaskOptions } from 'simple-git';
|
||||
import { v4 } from 'uuid';
|
||||
import { mkdir, rm } from 'fs/promises';
|
||||
import { URL } from 'node:url';
|
||||
|
@ -75,15 +75,19 @@ const isUrl = ( maybeURL: string ) => {
|
|||
/**
|
||||
* Clone a git repository.
|
||||
*
|
||||
* @param {string} repoPath - the path (either URL or file path) to the repo to clone.
|
||||
* @param {string} repoPath - the path (either URL or file path) to the repo to clone.
|
||||
* @param {TaskOptions} options - options to pass to simple-git.
|
||||
* @return {Promise<string>} the path to the cloned repo.
|
||||
*/
|
||||
export const cloneRepo = async ( repoPath: string ) => {
|
||||
export const cloneRepo = async (
|
||||
repoPath: string,
|
||||
options: TaskOptions = {}
|
||||
) => {
|
||||
const folderPath = join( tmpdir(), 'code-analyzer-tmp', v4() );
|
||||
mkdirSync( folderPath, { recursive: true } );
|
||||
|
||||
const git = simpleGit( { baseDir: folderPath } );
|
||||
await git.clone( repoPath, folderPath );
|
||||
await git.clone( repoPath, folderPath, options );
|
||||
|
||||
// If this is a local clone then the simplest way to maintain remote settings is to copy git config across
|
||||
if ( ! isUrl( repoPath ) ) {
|
||||
|
@ -96,18 +100,32 @@ export const cloneRepo = async ( repoPath: string ) => {
|
|||
return folderPath;
|
||||
};
|
||||
|
||||
/**
|
||||
* Clone a git repository without history.
|
||||
*
|
||||
* @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 cloneRepoShallow = async ( repoPath: string ) => {
|
||||
return await cloneRepo( repoPath, { '--depth': 1 } );
|
||||
};
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* @param {string} base - the base branch to checkout from. Defaults to trunk.
|
||||
* @param {TaskOptions} options - options to pass to simple-git.
|
||||
* @return {Promise<string>} the path to the cloned repo.
|
||||
*/
|
||||
export const sparseCheckoutRepo = async (
|
||||
githubRepoUrl: string,
|
||||
path: string,
|
||||
directories: string[]
|
||||
directories: string[],
|
||||
base = 'trunk',
|
||||
options: TaskOptions = {}
|
||||
) => {
|
||||
const folderPath = join( tmpdir(), path );
|
||||
|
||||
|
@ -117,13 +135,37 @@ export const sparseCheckoutRepo = async (
|
|||
|
||||
const git = simpleGit( { baseDir: folderPath } );
|
||||
|
||||
await git.clone( githubRepoUrl, folderPath );
|
||||
const cloneOptions = { '--no-checkout': null };
|
||||
await git.clone( githubRepoUrl, folderPath, {
|
||||
...cloneOptions,
|
||||
...options,
|
||||
} );
|
||||
await git.raw( 'sparse-checkout', 'init', { '--cone': null } );
|
||||
await git.raw( 'sparse-checkout', 'set', directories.join( ' ' ) );
|
||||
await git.checkout( base );
|
||||
|
||||
return folderPath;
|
||||
};
|
||||
|
||||
/**
|
||||
* Do a minimal sparse checkout of a github repo without history.
|
||||
*
|
||||
* @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 sparseCheckoutRepoShallow = async (
|
||||
githubRepoUrl: string,
|
||||
path: string,
|
||||
directories: string[],
|
||||
base = 'trunk'
|
||||
) => {
|
||||
return await sparseCheckoutRepo( githubRepoUrl, path, directories, base, {
|
||||
'--depth': 1,
|
||||
} );
|
||||
};
|
||||
|
||||
/**
|
||||
* checkoutRef - checkout a ref in a git repo.
|
||||
*
|
||||
|
|
Loading…
Reference in New Issue