Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add CLI for monorepo releases #8308

Merged
Merged
6 changes: 5 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@
"pre-push": "yarn --cwd packages/eui pre-push",
"preinstall": "echo \"\\x1b[K\\x1b[37;41mWarning: EUI has recently migrated to a monorepo structure. Please run EUI scripts like \\x1b[1;4myarn start\\x1b[0m\\x1b[37;41m or \\x1b[1;4myarn build\\x1b[0m\\x1b[37;41m from the \\x1b[1;4mpackages/eui\\x1b[0m\\x1b[37;41m directory instead!\n\nIf this is the first time you're running EUI after the monorepo migration, please run this first from the root repository's directory to clean up your local environment:\n \\x1b[1;4mrm -rf node_modules .cache-loader dist es lib optimize test-env types .eslintcache .loki reports docs .nyc_output eui.d.ts && yarn\\x1b[0m\\x1b[37;41m\n\nInstall process will continue in 10 seconds...\\x1b[0m\"; sleep 10",
"start": "echo '\\x1b[K\\x1b[37;41mPlease run this script from the \\x1b[1;4mpackages/eui\\x1b[0m\\x1b[37;41m directory instead\\x1b[0m'; exit 1",
"build": "echo '\\x1b[K\\x1b[37;41mPlease run this script from the \\x1b[1;4mpackages/eui\\x1b[0m\\x1b[37;41m directory instead\\x1b[0m'; exit 1"
"build": "echo '\\x1b[K\\x1b[37;41mPlease run this script from the \\x1b[1;4mpackages/eui\\x1b[0m\\x1b[37;41m directory instead\\x1b[0m'; exit 1",
"release": "node scripts/release"
},
"repository": {
"type": "git",
Expand All @@ -24,6 +25,9 @@
"devDependencies": {
"pre-push": "^0.1.4"
},
"dependencies": {
"@elastic/eui-release-cli": "link:packages/release-cli"
},
"resolutions": {
"prismjs": "1.27.0",
"react": "^18",
Expand Down
1 change: 1 addition & 0 deletions packages/eui-docgen/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
},
"private": true,
"dependencies": {
"@elastic/eui": "workspace:^",
"glob": "^11.0.0",
"react-docgen-typescript": "^2.2.2",
"ts-node": "^10.9.2",
Expand Down
8 changes: 8 additions & 0 deletions packages/release-cli/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Dependencies
/node_modules

# Production
/dist

yarn-debug.log*
yarn-error.log*
27 changes: 27 additions & 0 deletions packages/release-cli/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"name": "@elastic/eui-release-cli",
"private": true,
"version": "0.0.1",
"description": "",
"main": "dist/index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "tsc"
},
"repository": {
"type": "git",
"url": "https://github.com/tkajtoch/eui.git",
"directory": "packages/release-cli"
},
"devDependencies": {
"@types/prompts": "^2.4.9",
"typescript": "^5.7.3"
},
"dependencies": {
"chalk": "^4",
"glob": "^11.0.1",
"prompts": "^2.4.2",
"rimraf": "^6.0.1",
"yargs": "^17.7.2"
}
}
98 changes: 98 additions & 0 deletions packages/release-cli/src/cli.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

import yargs from 'yargs/yargs';
import { hideBin } from 'yargs/helpers';
import { release, type ReleaseType } from './release';
import { Logger } from './logger';
import { ValidationError } from './errors';

export const cli = () => {
yargs(hideBin(process.argv))
.command(
'run <type> [--tag] [--workspaces] [--allowCustom] [--verbose | -v]',
'Run the release process',
(yargs) => {
return yargs
.positional('type', {
type: 'string',
describe:
'Type of release to perform. Releases of type `official` will be tagged as `latest` in npm and are meant for official, stable builds only!',
choices: ['official', 'snapshot'] satisfies ReleaseType[],
demandOption: true,
})
.option('tag', {
type: 'string',
describe:
'npm tag for the release. It is forced to `latest` for official releases and defaults to `snapshot` for snapshot releases.',
})
.option('workspaces', {
type: 'string',
array: true,
describe:
'An optional space-separated list of workspaces to release. Defaults to all workspaces changed since the last release.',
})
.option('allowCustom', {
type: 'boolean',
default: false,
})
.option('verbose', {
alias: 'v',
type: 'boolean',
description: 'Enable verbose logging',
default: false,
})
.option('skipPrompts', {
type: 'boolean',
description:
'Skip user prompts and proceed with recommended settings. Use in CI only!',
default: false,
})
.option('useAuthToken', {
type: 'boolean',
description:
'Use npm auth token instead of the regular npm user authentication and one-time passwords (OTP). Use in CI only!',
default: false,
});
},
async (argv) => {
const {
type,
tag,
workspaces,
allowCustom,
verbose,
skipPrompts,
useAuthToken,
} = argv;
const logger = new Logger(verbose);

try {
await release({
type,
tag,
workspaces,
logger,
skipPrompts,
useAuthToken,
allowCustomReleases: allowCustom,
});
} catch (err) {
if (err instanceof ValidationError) {
// ValidationErrors don't need the stacktrace printed out
logger.error(err.toString());
} else {
logger.error(err);
}
process.exit(1);
}
}
)
.demandCommand(1)
.parse();
};
28 changes: 28 additions & 0 deletions packages/release-cli/src/errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

export class ValidationError extends Error {
public helpText: string | null = null;

constructor(message: string, helpText?: string) {
super(message);

if (helpText !== undefined) {
this.helpText = helpText;
}
}

toString() {
let finalHelpText = '';
if (this.helpText) {
finalHelpText += '\n\n';
finalHelpText += this.helpText;
}
return `${this.message}${finalHelpText}`;
}
}
65 changes: 65 additions & 0 deletions packages/release-cli/src/git_utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

import { promisify } from 'node:util';
import { exec } from 'node:child_process';

const execPromise = promisify(exec);

export const getCurrentBranch = async () => {
const result = await execPromise('git rev-parse --abbrev-ref HEAD');

return result.stdout.trim();
}

export const isWorkingTreeClean = async () => {
const gitStatusResult = await execPromise('git status --porcelain');

return gitStatusResult.stdout === '' && gitStatusResult.stderr === '';
}

export const getRemoteHeadCommitHash = async (branchName: string) => {
try {
const result = await execPromise(`git ls-remote --head --exit-code upstream refs/heads/${branchName}`);
return result.stdout.split('\t')[0];
} catch (err) {
// https://git-scm.com/docs/git-ls-remote#Documentation/git-ls-remote.txt---exit-code
if ((err as any).code === 2) {
// Remote ref not found
return '';
}

throw err;
}
}

export const getLocalHeadCommitHash = async () => {
const result = await execPromise('git rev-parse HEAD');

return result.stdout.trim();
};

export const getCommitMessage = async (commitHash: string) => {
// Well, technically this returns commit subject, but we don't care about the whole commit body
const result = await execPromise(`git log -1 --pretty=format:%s ${commitHash}`);
return result.stdout.trim();
};

export const stageFiles = async (files: string[]) => {
return execPromise(`git add ${files.join(' ')}`);
};

export const commitFiles = async (message: string, files: string[]) => {
// This isn't the best at handling unusual formatting like messages with quotes
return execPromise(`git commit ${files.join(' ')} -m "${message}"`);
}

export const isFileAddedToGit = async (file: string) => {
const result = await execPromise(`git ls-files --exclude-standard "${file}"`);
return result.stdout.length > 0;
}
11 changes: 11 additions & 0 deletions packages/release-cli/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

import { cli } from './cli';

cli();
37 changes: 37 additions & 0 deletions packages/release-cli/src/logger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

import chalk from 'chalk';

export class Logger {
private readonly PREFIX_DEBUG = chalk.gray('[debug]');
private readonly PREFIX_INFO = chalk.white('[info]');
private readonly PREFIX_WARNING = chalk.yellow('[warning]');
private readonly PREFIX_ERROR = chalk.red('[error]');

constructor(private readonly verbose: boolean) {}

debug(message: any, ...args: any) {
if (!this.verbose) {
return;
}
console.debug(this.PREFIX_DEBUG, message, ...args);
}

info(message: any, ...args: any) {
console.info(this.PREFIX_INFO, message, ...args);
}

warning(message: any, ...args: any) {
console.warn(this.PREFIX_WARNING, message, ...args);
}

error(message: any, ...args: any) {
console.error(this.PREFIX_ERROR, message, ...args);
}
}
23 changes: 23 additions & 0 deletions packages/release-cli/src/npm_utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

import { promisify } from 'node:util';
import { exec } from 'node:child_process';

const execPromise = promisify(exec);

export const getNpmPublishedVersions = async (packageName: string) => {
try {
const result = await execPromise(`npm view ${packageName} versions --json`);
return JSON.parse(result.stdout) as string[];
} catch (err) {
console.error(err);
}

return [];
}
Loading