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",
"cherry-pick": "node ./tools/cherry-pick/bin/run",
"sync-dependencies": "pnpm exec syncpack -- fix-mismatches",
"utils": "./tools/monorepo-utils/bin/run"
"utils": "node ./tools/monorepo-utils/dist/index.js"
},
"devDependencies": {
"@babel/preset-env": "^7.20.2",

View File

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

View File

@ -2,4 +2,19 @@
## 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",
"dependencies": {
"@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-schema": "^14.1.0",
"@types/uuid": "^9.0.1",
"chalk": "^4.1.2",
"commander": "^9.4.0",
"commander": "^10.0.1",
"dotenv": "^10.0.0",
"figlet": "^1.6.0",
"graphql": "^16.6.0",

View File

@ -20,10 +20,12 @@ import {
} from '../../../core/github/repo';
import { WPIncrement } from '../../../core/version';
import { Logger } from '../../../core/logger';
import { Options } from './types';
import { getEnvVar } from '../../../core/environment';
const getNextReleaseBranch = async ( options: Options ) => {
const getNextReleaseBranch = async ( options: {
owner?: string;
name?: string;
} ) => {
const latestReleaseVersion = await getLatestGithubReleaseVersion( options );
const nextReleaseVersion = WPIncrement( latestReleaseVersion );
const parsedNextReleaseVersion = parse( nextReleaseVersion );
@ -53,7 +55,7 @@ export const branchCommand = new Command( 'branch' )
'Branch to create the release branch from. Default: trunk',
'trunk'
)
.action( async ( options: Options ) => {
.action( async ( options ) => {
const { source, branch, owner, name, dryRun } = options;
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 { setGithubMilestoneOutputs } from './utils';
import { WPIncrement } from '../../../core/version';
import { Options } from './types';
import { Logger } from '../../../core/logger';
import { getEnvVar } from '../../../core/environment';
@ -32,7 +31,7 @@ export const milestoneCommand = new Command( 'milestone' )
'-m --milestone <milestone>',
'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 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 || '';
};
export const isGithubCI = () => {
return process.env.GITHUB_ACTIONS === 'true';
};

View File

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

View File

@ -3,5 +3,48 @@
*/
import { promisify } from 'util';
import { exec } from 'child_process';
import { RequestOptions, request } from 'https';
import { IncomingMessage } from 'http';
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
*/
import CodeFreeze from './code-freeze/commands';
import Slack from './slack/commands/slack';
import { Logger } from './core/logger';
import { isGithubCI } from './core/environment';
console.log(
chalk.rgb( 150, 88, 138 ).bold( figlet.textSync( 'WooCommerce Utilities' ) )
);
if ( ! isGithubCI() ) {
Logger.notice(
chalk
.rgb( 150, 88, 138 )
.bold( figlet.textSync( 'WooCommerce \n Utils' ) )
);
}
export const program = new Command()
const program = new Command()
.name( 'utils' )
.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 );
}
}
} );