Package release script: Prepare packages (#33515)

This commit is contained in:
Paul Sealock 2022-06-23 12:09:43 +12:00 committed by GitHub
parent 59427def84
commit 6dd78f0f62
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 506 additions and 0 deletions

View File

@ -1753,6 +1753,33 @@ importers:
tslib: 2.3.1
typescript: 4.4.4
tools/package-release:
specifiers:
'@oclif/core': ^1
'@oclif/plugin-help': ^5
'@oclif/plugin-plugins': ^2.0.1
'@types/node': ^16.9.4
'@woocommerce/eslint-plugin': workspace:*
globby: ^11
oclif: ^2
shx: ^0.3.3
ts-node: ^10.2.1
tslib: ^2.3.1
typescript: ^4.4.3
dependencies:
'@oclif/core': 1.3.4
'@oclif/plugin-help': 5.1.11
'@oclif/plugin-plugins': 2.1.0
devDependencies:
'@types/node': 16.10.3
'@woocommerce/eslint-plugin': link:../../packages/js/eslint-plugin
globby: 11.1.0
oclif: 2.4.5
shx: 0.3.4
ts-node: 10.5.0_506ca6ef959d35afcce359030b1bc9ff
tslib: 2.3.1
typescript: 4.6.2
packages:
/@ampproject/remapping/2.1.2:

View File

@ -5,3 +5,4 @@ packages:
- 'tools/monorepo-merge'
- 'tools/code-analyzer'
- 'tools/create-extension'
- 'tools/package-release'

View File

@ -0,0 +1 @@
/dist

View File

@ -0,0 +1,3 @@
{
"extends": [ "plugin:@woocommerce/eslint-plugin/recommended" ]
}

1
tools/package-release/.gitignore vendored Normal file
View File

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

17
tools/package-release/bin/dev Executable file
View File

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

View File

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

5
tools/package-release/bin/run Executable file
View File

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

View File

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

View File

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

View File

@ -0,0 +1,107 @@
/**
* External dependencies
*/
import { execSync } from 'child_process';
import { readdirSync } from 'fs';
import { join } from 'path';
/**
* Internal dependencies
*/
import { getFilepathFromPackageName } from './validate';
/**
* Call changelogger's next version function to get the version for the next release.
*
* @param {string} name Package name.
* @return {string} Next release version.
*/
export const getNextVersion = ( name: string ) => {
try {
const cwd = getFilepathFromPackageName( name );
return execSync( './vendor/bin/changelogger version next', {
cwd,
encoding: 'utf-8',
} ).trim();
} catch ( e ) {
let message = '';
if ( e instanceof Error ) {
message = e.message;
throw new Error( message );
}
}
};
/**
* Call Changelogger's validate function on changelog entries.
*
* @param {string} name
* @return {Error|void} Output of changelogger exec.
*/
export const validateChangelogEntries = ( name: string ) => {
try {
const cwd = getFilepathFromPackageName( name );
return execSync( './vendor/bin/changelogger validate', {
cwd,
encoding: 'utf-8',
} );
} catch ( e ) {
let message = '';
if ( e instanceof Error ) {
message = e.message;
throw new Error( message );
}
}
};
/**
* Write the changelog.
*
* @param {string} name Package name.
*/
export const writeChangelog = ( name: string ) => {
try {
const cwd = getFilepathFromPackageName( name );
execSync( './vendor/bin/changelogger write', {
cwd,
encoding: 'utf-8',
} );
} catch ( e ) {
let message = '';
if ( e instanceof Error ) {
message = e.message;
throw new Error(
message + ' - Package may not have changelog entries.'
);
}
}
};
/**
* Determine if a package has changelogs to release.
*
* @param {string} name Package name.
* @return {boolean} If there are changelogs.
*/
export const hasChangelogs = ( name: string ): boolean | void => {
try {
const changelogDir = join(
getFilepathFromPackageName( name ),
'changelog'
);
const changelogDirContents = readdirSync( changelogDir, {
encoding: 'utf-8',
} );
return (
changelogDirContents.filter( ( entry ) => entry !== '.gitkeep' )
.length > 0
);
} catch ( e ) {
let message = '';
if ( e instanceof Error ) {
message = e.message;
throw new Error( message );
}
}
};

View File

@ -0,0 +1,130 @@
/**
* External dependencies
*/
import { CliUx, Command, Flags } from '@oclif/core';
import { readFileSync, writeFileSync } from 'fs';
/**
* Internal dependencies
*/
import {
getAllPackges,
validatePackage,
getFilepathFromPackageName,
} from '../../validate';
import {
getNextVersion,
validateChangelogEntries,
writeChangelog,
hasChangelogs,
} from '../../changelogger';
/**
* PackageRelease class
*/
export default class PackageRelease extends Command {
/**
* CLI description
*/
static description = 'Release Monorepo JS packages';
/**
* CLI arguments
*/
static args = [
{
name: 'packages',
description:
'Package to release, or packages to release separated by commas.',
required: false,
},
];
/**
* CLI flags.
*/
static flags = {
all: Flags.boolean( {
char: 'a',
default: false,
description: 'Perform prepare function on all packages.',
} ),
};
/**
* This method is called to execute the command
*/
async run(): Promise< void > {
const { args, flags } = await this.parse( PackageRelease );
if ( ! args.packages && ! flags.all ) {
this.error( 'No packages supplied.' );
}
if ( flags.all ) {
this.preparePackages( getAllPackges() );
return;
}
const packages = args.packages.split( ',' );
packages.forEach( ( name: string ) =>
validatePackage( name, ( e: string ): void => this.error( e ) )
);
this.preparePackages( packages );
}
/**
* Prepare packages for release by creating the changelog and bumping version.
*
* @param {Array<string>} packages Packages to prepare.
*/
private preparePackages( packages: Array< string > ) {
packages.forEach( ( name ) => {
CliUx.ux.action.start( `Preparing ${ name }` );
try {
if ( hasChangelogs( name ) ) {
validateChangelogEntries( name );
const nextVersion = getNextVersion( name );
writeChangelog( name );
if ( nextVersion ) {
this.bumpPackageVersion( name, nextVersion );
}
} else {
this.log( `Skipping ${ name }, no changelogs available.` );
}
} catch ( e ) {
if ( e instanceof Error ) {
this.error( e.message );
}
}
CliUx.ux.action.stop();
} );
}
/**
* Update the version number in package.json.
*
* @param {string} name Package name.
* @param {string} version Next version.
*/
private bumpPackageVersion( name: string, version: string ) {
const filepath = getFilepathFromPackageName( name );
const packageJsonFilepath = `${ filepath }/package.json`;
try {
const packageJson = JSON.parse(
readFileSync( packageJsonFilepath, 'utf8' )
);
packageJson.version = version;
writeFileSync(
packageJsonFilepath,
JSON.stringify( packageJson, null, '\t' ) + '\n'
);
} catch ( e ) {
this.error( `Can't bump version for ${ name }.` );
}
}
}

View File

@ -0,0 +1,17 @@
/**
* External dependencies
*/
import { dirname } from 'path';
// Escape from ./tools/package-release/src
export const MONOREPO_ROOT = dirname( dirname( dirname( __dirname ) ) );
// Packages that are not meant to be released by monorepo team for whatever reason.
export const excludedPackages = [
'@woocommerce/admin-e2e-tests',
'@woocommerce/api',
'@woocommerce/api-core-tests',
'@woocommerce/e2e-core-tests',
'@woocommerce/e2e-environment',
'@woocommerce/e2e-utils',
];

View File

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

View File

@ -0,0 +1,113 @@
/**
* External dependencies
*/
import { existsSync, readFileSync, readdirSync } from 'fs';
import { join } from 'path';
/**
* Internal dependencies
*/
import { MONOREPO_ROOT, excludedPackages } from './const';
/**
* Get filepath for a given package name.
*
* @param {string} name package name.
* @return {string} Absolute path for the package.
*/
export const getFilepathFromPackageName = ( name: string ): string =>
join( MONOREPO_ROOT, 'packages/js', name.replace( '@woocommerce', '' ) );
/**
* Check if package is valid and can be deployed to NPM.
*
* @param {string} name package name.
* @return {boolean} true if the package is private.
*/
export const isValidPackage = ( name: string ): boolean => {
const filepath = getFilepathFromPackageName( name );
const packageJsonFilepath = `${ filepath }/package.json`;
const packageJsonExists = existsSync( packageJsonFilepath );
if ( ! packageJsonExists ) {
return false;
}
const packageJson = JSON.parse(
readFileSync( packageJsonFilepath, 'utf8' )
);
if ( name !== packageJson.name ) {
return false;
}
const isPrivatePackage = !! packageJson.private;
if ( isPrivatePackage ) {
return false;
}
return true;
};
/**
* Validate package name.
*
* @param {string} name package name.
* @param {Function} error Error logging function.
*/
export const validatePackageName = (
name: string,
error: ( s: string ) => void
) => {
const filepath = getFilepathFromPackageName( name );
try {
const exists = existsSync( filepath );
if ( ! exists ) {
throw new Error();
}
} catch ( e ) {
error( `${ name } does not exist as a package.` );
}
};
/**
* Get all releaseable package names.
*
* @return {Array<string>} Package names.
*/
export const getAllPackges = (): Array< string > => {
const jsPackageFolders = readdirSync(
join( MONOREPO_ROOT, 'packages/js' ),
{
encoding: 'utf-8',
}
);
return jsPackageFolders
.map( ( folder ) => '@woocommerce/' + folder )
.filter( ( name ) => {
if ( excludedPackages.includes( name ) ) {
return false;
}
return isValidPackage( name );
} );
};
/**
* Validate a package.
*
* @param {string} name package name.
* @param {Function} error Error logging function.
*/
export const validatePackage = (
name: string,
error: ( s: string ) => void
) => {
validatePackageName( name, error );
if ( ! isValidPackage( name ) ) {
error(
`${ name } is not a valid package. It may be private or incorrectly configured.`
);
}
};

View File

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