Add monorepo util to notify slack, add improvements to calling utils and type clean up. (#38185)

This commit is contained in:
Sam Seay 2023-05-11 17:23:19 +12:00 committed by GitHub
parent b581db2a7e
commit 95ac08739b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 239 additions and 70 deletions

View File

@ -30,7 +30,7 @@
"create-extension": "node ./tools/create-extension/index.js", "create-extension": "node ./tools/create-extension/index.js",
"cherry-pick": "node ./tools/cherry-pick/bin/run", "cherry-pick": "node ./tools/cherry-pick/bin/run",
"sync-dependencies": "pnpm exec syncpack -- fix-mismatches", "sync-dependencies": "pnpm exec syncpack -- fix-mismatches",
"utils": "./tools/monorepo-utils/bin/run" "utils": "node ./tools/monorepo-utils/dist/index.js"
}, },
"devDependencies": { "devDependencies": {
"@babel/preset-env": "^7.20.2", "@babel/preset-env": "^7.20.2",

View File

@ -3242,8 +3242,8 @@ importers:
specifier: ^1.10.0 specifier: ^1.10.0
version: 1.10.0 version: 1.10.0
'@commander-js/extra-typings': '@commander-js/extra-typings':
specifier: ^0.1.0 specifier: ^10.0.3
version: 0.1.0(commander@9.4.0) version: 10.0.3(commander@10.0.1)
'@octokit/graphql': '@octokit/graphql':
specifier: 4.8.0 specifier: 4.8.0
version: 4.8.0 version: 4.8.0
@ -3257,8 +3257,8 @@ importers:
specifier: ^4.1.2 specifier: ^4.1.2
version: 4.1.2 version: 4.1.2
commander: commander:
specifier: ^9.4.0 specifier: ^10.0.1
version: 9.4.0 version: 10.0.1
dotenv: dotenv:
specifier: ^10.0.0 specifier: ^10.0.0
version: 10.0.0 version: 10.0.0
@ -3871,7 +3871,7 @@ packages:
'@babel/core': ^7.0.0-0 '@babel/core': ^7.0.0-0
dependencies: dependencies:
'@babel/core': 7.21.3 '@babel/core': 7.21.3
'@jridgewell/trace-mapping': 0.3.16 '@jridgewell/trace-mapping': 0.3.17
commander: 4.1.1 commander: 4.1.1
convert-source-map: 1.8.0 convert-source-map: 1.8.0
fs-readdir-recursive: 1.1.0 fs-readdir-recursive: 1.1.0
@ -8720,9 +8720,9 @@ packages:
'@babel/core': 7.21.3 '@babel/core': 7.21.3
'@babel/helper-annotate-as-pure': 7.16.7 '@babel/helper-annotate-as-pure': 7.16.7
'@babel/helper-module-imports': 7.16.7 '@babel/helper-module-imports': 7.16.7
'@babel/helper-plugin-utils': 7.20.2 '@babel/helper-plugin-utils': 7.18.9
'@babel/plugin-syntax-jsx': 7.16.7(@babel/core@7.21.3) '@babel/plugin-syntax-jsx': 7.16.7(@babel/core@7.21.3)
'@babel/types': 7.21.3 '@babel/types': 7.17.0
dev: true dev: true
/@babel/plugin-transform-react-jsx@7.19.0(@babel/core@7.12.9): /@babel/plugin-transform-react-jsx@7.19.0(@babel/core@7.12.9):
@ -8951,8 +8951,8 @@ packages:
dependencies: dependencies:
'@babel/core': 7.21.3 '@babel/core': 7.21.3
'@babel/helper-module-imports': 7.16.0 '@babel/helper-module-imports': 7.16.0
'@babel/helper-plugin-utils': 7.14.5 '@babel/helper-plugin-utils': 7.20.2
babel-plugin-polyfill-corejs2: 0.3.0(@babel/core@7.21.3) babel-plugin-polyfill-corejs2: 0.3.3(@babel/core@7.21.3)
babel-plugin-polyfill-corejs3: 0.4.0(@babel/core@7.21.3) babel-plugin-polyfill-corejs3: 0.4.0(@babel/core@7.21.3)
babel-plugin-polyfill-regenerator: 0.3.0(@babel/core@7.21.3) babel-plugin-polyfill-regenerator: 0.3.0(@babel/core@7.21.3)
semver: 6.3.0 semver: 6.3.0
@ -10461,6 +10461,14 @@ packages:
commander: 9.4.0 commander: 9.4.0
dev: false dev: false
/@commander-js/extra-typings@10.0.3(commander@10.0.1):
resolution: {integrity: sha512-OIw28QV/GlP8k0B5CJTRsl8IyNvd0R8C8rfo54Yz9P388vCNDgdNrFlKxZTGqps+5j6lSw3Ss9JTQwcur1w1oA==}
peerDependencies:
commander: 10.0.x
dependencies:
commander: 10.0.1
dev: false
/@cspotcode/source-map-support@0.8.1: /@cspotcode/source-map-support@0.8.1:
resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==}
engines: {node: '>=12'} engines: {node: '>=12'}
@ -11961,6 +11969,7 @@ packages:
dependencies: dependencies:
'@jridgewell/resolve-uri': 3.1.0 '@jridgewell/resolve-uri': 3.1.0
'@jridgewell/sourcemap-codec': 1.4.14 '@jridgewell/sourcemap-codec': 1.4.14
dev: true
/@jridgewell/trace-mapping@0.3.17: /@jridgewell/trace-mapping@0.3.17:
resolution: {integrity: sha512-MCNzAp77qzKca9+W/+I0+sEpaUnZoeasnghNeVc41VZCEKaCH73Vq3BZZ/SzWIgrqE4H4ceI+p+b6C0mHf9T4g==} resolution: {integrity: sha512-MCNzAp77qzKca9+W/+I0+sEpaUnZoeasnghNeVc41VZCEKaCH73Vq3BZZ/SzWIgrqE4H4ceI+p+b6C0mHf9T4g==}
@ -20854,8 +20863,8 @@ packages:
peerDependencies: peerDependencies:
postcss: ^8.1.0 postcss: ^8.1.0
dependencies: dependencies:
browserslist: 4.21.4 browserslist: 4.20.2
caniuse-lite: 1.0.30001418 caniuse-lite: 1.0.30001352
fraction.js: 4.2.0 fraction.js: 4.2.0
normalize-range: 0.1.2 normalize-range: 0.1.2
picocolors: 1.0.0 picocolors: 1.0.0
@ -22398,7 +22407,6 @@ packages:
escalade: 3.1.1 escalade: 3.1.1
node-releases: 2.0.6 node-releases: 2.0.6
picocolors: 1.0.0 picocolors: 1.0.0
dev: true
/browserslist@4.20.4: /browserslist@4.20.4:
resolution: {integrity: sha512-ok1d+1WpnU24XYN7oC3QWgTyMhY/avPJ/r9T00xxvUOIparA/gc+UPUMaod3i+G6s+nI2nUb9xZ5k794uIwShw==} resolution: {integrity: sha512-ok1d+1WpnU24XYN7oC3QWgTyMhY/avPJ/r9T00xxvUOIparA/gc+UPUMaod3i+G6s+nI2nUb9xZ5k794uIwShw==}
@ -23443,6 +23451,11 @@ packages:
engines: {node: '>=14'} engines: {node: '>=14'}
dev: true dev: true
/commander@10.0.1:
resolution: {integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==}
engines: {node: '>=14'}
dev: false
/commander@2.1.0: /commander@2.1.0:
resolution: {integrity: sha512-J2wnb6TKniXNOtoHS8TSrG9IOQluPrsmyAJ8oCUJOBmv+uLBCyPYAZkD2jFvw2DCzIXNnISIM01NIvr35TkBMQ==} resolution: {integrity: sha512-J2wnb6TKniXNOtoHS8TSrG9IOQluPrsmyAJ8oCUJOBmv+uLBCyPYAZkD2jFvw2DCzIXNnISIM01NIvr35TkBMQ==}
engines: {node: '>= 0.6.x'} engines: {node: '>= 0.6.x'}

View File

@ -2,4 +2,19 @@
## Description ## Description
Monorepo utilities and tooling. A set of CLI tools and scripts for managing the WooCommerce monorepo.
## Usage
This command is built on postinstall and can be run from the root of the project.
To see a list of available commands you can run this from project root:
```
pnpm utils
```
## Development
During development you can watch for changes via `pnpm start` in this directory, this will
update the script referenced at the root package.json so you will see immediate changes as you
re-run the CLI.

View File

@ -1,25 +0,0 @@
#!/usr/bin/env node
const { existsSync } = require( 'fs' );
const chalk = require( 'chalk' );
const path = require( 'path' );
const nodeModulesDirectory = path.join( __dirname, '../', 'node_modules' );
if ( ! existsSync( nodeModulesDirectory ) ) {
console.log(
chalk.red(
'The @woocommerce/monorepo-utils must be built before running the CLI.'
)
);
console.log(
chalk.yellow(
'run `pnpm run build` from the root of the monorepo-utils package. or `pnpm install --filter monorepo-utils` from project root.'
)
);
process.exit( 1 );
}
const { program } = require( '../dist/index' );
program.parse( process.argv );

View File

@ -9,12 +9,12 @@
"main": "dist/index.js", "main": "dist/index.js",
"dependencies": { "dependencies": {
"@actions/core": "^1.10.0", "@actions/core": "^1.10.0",
"@commander-js/extra-typings": "^0.1.0", "@commander-js/extra-typings": "^10.0.3",
"@octokit/graphql": "4.8.0", "@octokit/graphql": "4.8.0",
"@octokit/graphql-schema": "^14.1.0", "@octokit/graphql-schema": "^14.1.0",
"@types/uuid": "^9.0.1", "@types/uuid": "^9.0.1",
"chalk": "^4.1.2", "chalk": "^4.1.2",
"commander": "^9.4.0", "commander": "^10.0.1",
"dotenv": "^10.0.0", "dotenv": "^10.0.0",
"figlet": "^1.6.0", "figlet": "^1.6.0",
"graphql": "^16.6.0", "graphql": "^16.6.0",

View File

@ -20,10 +20,12 @@ import {
} from '../../../core/github/repo'; } from '../../../core/github/repo';
import { WPIncrement } from '../../../core/version'; import { WPIncrement } from '../../../core/version';
import { Logger } from '../../../core/logger'; import { Logger } from '../../../core/logger';
import { Options } from './types';
import { getEnvVar } from '../../../core/environment'; import { getEnvVar } from '../../../core/environment';
const getNextReleaseBranch = async ( options: Options ) => { const getNextReleaseBranch = async ( options: {
owner?: string;
name?: string;
} ) => {
const latestReleaseVersion = await getLatestGithubReleaseVersion( options ); const latestReleaseVersion = await getLatestGithubReleaseVersion( options );
const nextReleaseVersion = WPIncrement( latestReleaseVersion ); const nextReleaseVersion = WPIncrement( latestReleaseVersion );
const parsedNextReleaseVersion = parse( nextReleaseVersion ); const parsedNextReleaseVersion = parse( nextReleaseVersion );
@ -53,7 +55,7 @@ export const branchCommand = new Command( 'branch' )
'Branch to create the release branch from. Default: trunk', 'Branch to create the release branch from. Default: trunk',
'trunk' 'trunk'
) )
.action( async ( options: Options ) => { .action( async ( options ) => {
const { source, branch, owner, name, dryRun } = options; const { source, branch, owner, name, dryRun } = options;
const isGithub = getEnvVar( 'CI' ); const isGithub = getEnvVar( 'CI' );

View File

@ -1,8 +0,0 @@
export type Options = {
github?: boolean;
dryRun?: boolean;
owner?: string;
name?: string;
branch?: string;
source?: string;
};

View File

@ -11,7 +11,6 @@ import { getLatestGithubReleaseVersion } from '../../../core/github/repo';
import { octokitWithAuth } from '../../../core/github/api'; import { octokitWithAuth } from '../../../core/github/api';
import { setGithubMilestoneOutputs } from './utils'; import { setGithubMilestoneOutputs } from './utils';
import { WPIncrement } from '../../../core/version'; import { WPIncrement } from '../../../core/version';
import { Options } from './types';
import { Logger } from '../../../core/logger'; import { Logger } from '../../../core/logger';
import { getEnvVar } from '../../../core/environment'; import { getEnvVar } from '../../../core/environment';
@ -32,7 +31,7 @@ export const milestoneCommand = new Command( 'milestone' )
'-m --milestone <milestone>', '-m --milestone <milestone>',
'Milestone to create. Next milestone is gathered from Github if none is supplied' 'Milestone to create. Next milestone is gathered from Github if none is supplied'
) )
.action( async ( options: Options ) => { .action( async ( options ) => {
const { owner, name, dryRun, milestone } = options; const { owner, name, dryRun, milestone } = options;
const isGithub = getEnvVar( 'CI' ); const isGithub = getEnvVar( 'CI' );

View File

@ -1,7 +0,0 @@
export type Options = {
github?: boolean;
dryRun?: boolean;
owner?: string;
name?: string;
milestone?: string;
};

View File

@ -14,3 +14,7 @@ export const getEnvVar = ( varName: string, isRequired = false ) => {
return value || ''; return value || '';
}; };
export const isGithubCI = () => {
return process.env.GITHUB_ACTIONS === 'true';
};

View File

@ -7,7 +7,7 @@ import chalk from 'chalk';
/** /**
* Internal dependencies * Internal dependencies
*/ */
import { getEnvVar } from './environment'; import { getEnvVar, isGithubCI } from './environment';
const LOGGING_LEVELS: Record< string, number > = { const LOGGING_LEVELS: Record< string, number > = {
verbose: 3, verbose: 3,
@ -28,7 +28,7 @@ export class Logger {
static error( err: unknown, failOnErr = true ) { static error( err: unknown, failOnErr = true ) {
if ( Logger.loggingLevel >= LOGGING_LEVELS.error ) { if ( Logger.loggingLevel >= LOGGING_LEVELS.error ) {
if ( err instanceof Error ) { if ( err instanceof Error ) {
error( chalk.red( err.message ) ); error( chalk.red( `${ err.message }\n${ err.stack }` ) );
} else if ( typeof err === 'string' ) { } else if ( typeof err === 'string' ) {
error( chalk.red( err ) ); error( chalk.red( err ) );
} else { } else {
@ -55,21 +55,26 @@ export class Logger {
} }
static startTask( message: string ) { static startTask( message: string ) {
if ( Logger.loggingLevel > LOGGING_LEVELS.silent ) { if ( Logger.loggingLevel > LOGGING_LEVELS.silent && ! isGithubCI() ) {
const spinner = ora( chalk.green( `${ message }...` ) ).start(); const spinner = ora( chalk.green( `${ message }...` ) ).start();
Logger.lastSpinner = spinner; Logger.lastSpinner = spinner;
} else if ( isGithubCI() ) {
Logger.notice( message );
} }
} }
static endTask() { static endTask() {
if ( if (
Logger.loggingLevel > LOGGING_LEVELS.silent && Logger.loggingLevel > LOGGING_LEVELS.silent &&
Logger.lastSpinner Logger.lastSpinner &&
! isGithubCI()
) { ) {
Logger.lastSpinner.succeed( Logger.lastSpinner.succeed(
`${ Logger.lastSpinner.text } complete.` `${ Logger.lastSpinner.text } complete.`
); );
Logger.lastSpinner = null; Logger.lastSpinner = null;
} else if ( isGithubCI() ) {
Logger.notice( 'Task complete.' );
} }
} }
} }

View File

@ -3,5 +3,48 @@
*/ */
import { promisify } from 'util'; import { promisify } from 'util';
import { exec } from 'child_process'; import { exec } from 'child_process';
import { RequestOptions, request } from 'https';
import { IncomingMessage } from 'http';
export const execAsync = promisify( exec ); export const execAsync = promisify( exec );
// Map just the raw value types of IncomingMessage to a new type for the response which includes a body string.
type HttpsResponse = {
// I think it's fine to use this type this way just to exclude functions from the IncomingMessage type.
// eslint-disable-next-line @typescript-eslint/ban-types
[ K in keyof IncomingMessage as IncomingMessage[ K ] extends Function
? never
: K ]: IncomingMessage[ K ];
} & {
body: string;
};
// A wrapper around https.request that returns a promise encapulating the response body and other response attributes.
export const requestAsync = (
options: string | RequestOptions | URL,
data?: string | Uint8Array | Buffer
) => {
return new Promise< HttpsResponse >( ( resolve, reject ) => {
const req = request( options, ( res ) => {
let body = '';
res.setEncoding( 'utf8' );
res.on( 'data', ( chunk ) => {
body += chunk;
} );
res.on( 'end', () => {
const httpsResponse: HttpsResponse = {
...res,
body,
};
resolve( httpsResponse );
} );
} );
req.on( 'error', ( err ) => {
reject( err );
} );
if ( data ) {
req.write( data );
}
req.end();
} );
};

View File

@ -9,12 +9,39 @@ import chalk from 'chalk';
* Internal dependencies * Internal dependencies
*/ */
import CodeFreeze from './code-freeze/commands'; import CodeFreeze from './code-freeze/commands';
import Slack from './slack/commands/slack';
import { Logger } from './core/logger';
import { isGithubCI } from './core/environment';
console.log( if ( ! isGithubCI() ) {
chalk.rgb( 150, 88, 138 ).bold( figlet.textSync( 'WooCommerce Utilities' ) ) Logger.notice(
); chalk
.rgb( 150, 88, 138 )
.bold( figlet.textSync( 'WooCommerce \n Utils' ) )
);
}
export const program = new Command() const program = new Command()
.name( 'utils' ) .name( 'utils' )
.description( 'Monorepo utilities' ) .description( 'Monorepo utilities' )
.addCommand( CodeFreeze ); .addCommand( CodeFreeze )
.addCommand( Slack );
program.exitOverride();
const run = async () => {
try {
// parseAsync handles cases where the action is async and not async.
await program.parseAsync( process.argv );
} catch ( e ) {
// if github ci, always error
if ( isGithubCI() ) {
Logger.error( e );
} else if ( e.code !== 'commander.help' ) {
// if not github ci, only error if not help
Logger.error( e );
}
}
};
run();

View File

@ -0,0 +1,5 @@
# Slack Utilities
Utilities for automated posting to Slack.
To see available commands run `pnpm utils slack --help` from the project root.

View File

@ -0,0 +1,19 @@
/**
* External dependencies
*/
import { Command } from '@commander-js/extra-typings';
/**
* Internal dependencies
*/
import { slackMessageCommand } from './slack-message';
/**
* Internal dependencies
*/
const program = new Command( 'slack' )
.description( 'Slack message sending utilities' )
.addCommand( slackMessageCommand, { isDefault: true } );
export default program;

View File

@ -0,0 +1,77 @@
/**
* External dependencies
*/
import { Command } from '@commander-js/extra-typings';
/**
* Internal dependencies
*/
import { Logger } from '../../../core/logger';
import { requestAsync } from '../../../core/util';
type SlackResponse = {
ok: boolean;
error?: string;
};
export const slackMessageCommand = new Command( 'message' )
.description( 'Send a plain-text message to a slack channel' )
.argument(
'<token>',
'Slack authentication token bearing required scopes.'
)
.argument( '<text>', 'Text based message to send to the slack channel.' )
.argument(
'<channels...>',
'Slack channels to send the message to. Pass as many as you like.'
)
.option(
'--dont-fail',
'Do not fail the command if a message fails to send to any channel.'
)
.action( async ( token, text, channels, { dontFail } ) => {
Logger.startTask(
`Attempting to send message to Slack for channels: ${ channels.join(
','
) }`
);
const shouldFail = ! dontFail;
for ( const channel of channels ) {
// Define the request options
const options = {
hostname: 'slack.com',
path: '/api/chat.postMessage',
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${ token }`,
},
};
try {
const { statusCode, body } = await requestAsync(
options,
JSON.stringify( { channel, text } )
);
Logger.endTask();
const response = JSON.parse( body ) as SlackResponse;
if ( ! response.ok || statusCode !== 200 ) {
Logger.error(
`Slack API returned an error: ${ response?.error }, message failed to send to ${ channel }.`,
shouldFail
);
} else {
Logger.notice(
`Slack message sent successfully to channel: ${ channel }`
);
}
} catch ( e: unknown ) {
Logger.error( e, shouldFail );
}
}
} );