Compare commits

...

3 Commits

Author SHA1 Message Date
Joey Perrott cbe11f92ec fixup! feat(dev-infra): create tooling to check burn rate of credit usage on CircleCI 2020-11-05 11:49:24 -08:00
Joey Perrott 16b6cac4ed WIP 2020-11-05 10:47:01 -08:00
Joey Perrott a4d3fadd31 feat(dev-infra): create tooling to check burn rate of credit usage on CircleCI
To assist in tracking credit usage on CircleCI, this tooling allows for checking
the burn rate of credit usage in a repository.
2020-11-05 10:37:49 -08:00
10 changed files with 544 additions and 122 deletions
+154 -122
View File
@@ -12,6 +12,9 @@
# https://circleci.com/docs/2.0/reusing-config/#getting-started-with-config-reuse
version: 2.1
orbs:
slack: circleci/slack@4.1.1
# We don't want to include the current branch name in the cache key because that would prevent
# PRs from being able to restore the cache since the branch names are always different for PRs.
# The cache key should only consist of dynamic values that change whenever something in the
@@ -250,6 +253,23 @@ jobs:
- ./ng
- ./bazel_repository_cache
burn_rate:
executor: default-executor
steps:
- checkout
- run:
name: Running Yarn install
command: yarn install --frozen-lockfile --non-interactive
# Yarn's requests sometimes take more than 10mins to complete.
no_output_timeout: 45m
- run: yarn -s ng-dev ci check-burn-rate --ciToken $CIRCLE_CI_TOKEN
- slack/notify:
message: yarn -s ng-dev ci check-burn-rate --ciToken $CIRCLE_CI_TOKEN
webhook: $SLACK_BURN_RATE_WEBHOOK_URL
lint:
executor: default-executor
steps:
@@ -782,128 +802,140 @@ workflows:
filters:
branches:
ignore: g3
- lint:
requires:
- setup
- test:
requires:
- setup
- test_ivy_aot:
requires:
- setup
- build-npm-packages:
requires:
- setup
- build-ivy-npm-packages:
requires:
- setup
- legacy-unit-tests-saucelabs:
requires:
- setup
- test_aio:
requires:
- setup
- deploy_aio:
requires:
- test_aio
- test_aio_local:
requires:
- build-npm-packages
- test_aio_local:
name: test_aio_local_viewengine
viewengine: true
requires:
- build-npm-packages
- test_aio_tools:
requires:
- build-npm-packages
- test_docs_examples:
requires:
- build-npm-packages
- test_docs_examples:
name: test_docs_examples_viewengine
viewengine: true
requires:
- build-npm-packages
- aio_preview:
# Only run on PR builds. (There can be no previews for non-PR builds.)
<<: *only_on_pull_requests
requires:
- setup
- test_aio_preview:
requires:
- aio_preview
- publish_packages_as_artifacts:
requires:
- build-npm-packages
- publish_snapshot:
# Note: no filters on this job because we want it to run for all upstream branches
# We'd really like to filter out pull requests here, but not yet available:
# https://discuss.circleci.com/t/workflows-pull-request-filter/14396/4
# Instead, the job just exits immediately at the first step.
requires:
# Only publish if tests and integration tests pass
- test
- test_ivy_aot
# Only publish if `aio`/`docs` tests using the locally built Angular packages pass
- test_aio_local
- test_aio_local_viewengine
- test_docs_examples
- test_docs_examples_viewengine
# Get the artifacts to publish from the build-packages-dist job
# since the publishing script expects the legacy outputs layout.
- build-npm-packages
- build-ivy-npm-packages
- legacy-unit-tests-saucelabs
# Temporarily disabled components-repo-unit-tests to update rules_nodejs to 2.0.0. Breaking changes in
# rules_nodejs create a dependency sandwich between angular/angular & angular/components that are very
# difficult and time consuming to resolve and involve patching @angular/bazel in components repo such
# as https://github.com/angular/components/commit/9e7ba251207df77164d73d66620e619bcbc4d2ad. It is simpler to
# 1) land angular/angular upgrade to rule_nodejs 2.0.0 which has breaking changes
# 2) land angular/components upgrade to rules_nodejs 2.0.0 using the @angular/bazel builds snapshot
# 3) update angular/angular to the landed components commit and re-enable these tests
# - components-repo-unit-tests:
# requires:
# - build-npm-packages
- test_zonejs:
requires:
- setup
- test_win:
requires:
- setup
- test_ivy_aot_win:
requires:
- setup
monitoring:
jobs:
- setup
- aio_monitoring_stable:
- burn_rate:
requires:
- setup
- aio_monitoring_next:
requires:
- setup
- saucelabs_ivy:
# Testing saucelabs via Bazel currently taking longer than the legacy saucelabs job as it
# each karma_web_test target is provisioning and tearing down browsers which is adding
# a lot of overhead. Running once daily on master only to avoid wasting resources and
# slowing down CI for PRs.
# TODO: Run this job on all branches (including PRs) once karma_web_test targets can
# share provisioned browsers and we can remove the legacy saucelabs job.
requires:
- setup
- saucelabs_view_engine:
# Testing saucelabs via Bazel currently taking longer than the legacy saucelabs job as it
# each karma_web_test target is provisioning and tearing down browsers which is adding
# a lot of overhead. Running once daily on master only to avoid wasting resources and
# slowing down CI for PRs.
# TODO: Run this job on all branches (including PRs) once karma_web_test targets can
# share provisioned browsers and we can remove the legacy saucelabs job.
requires:
- setup
triggers:
- schedule:
<<: *only_on_master
# Runs monitoring jobs at 10:00AM every day.
cron: "0 10 * * *"
context:
- burn-rate
# - lint:
# requires:
# - setup
# - test:
# requires:
# - setup
# - test_ivy_aot:
# requires:
# - setup
# - build-npm-packages:
# requires:
# - setup
# - build-ivy-npm-packages:
# requires:
# - setup
# - legacy-unit-tests-saucelabs:
# requires:
# - setup
# - test_aio:
# requires:
# - setup
# - deploy_aio:
# requires:
# - test_aio
# - test_aio_local:
# requires:
# - build-npm-packages
# - test_aio_local:
# name: test_aio_local_viewengine
# viewengine: true
# requires:
# - build-npm-packages
# - test_aio_tools:
# requires:
# - build-npm-packages
# - test_docs_examples:
# requires:
# - build-npm-packages
# - test_docs_examples:
# name: test_docs_examples_viewengine
# viewengine: true
# requires:
# - build-npm-packages
# - aio_preview:
# # Only run on PR builds. (There can be no previews for non-PR builds.)
# <<: *only_on_pull_requests
# requires:
# - setup
# - test_aio_preview:
# requires:
# - aio_preview
# - publish_packages_as_artifacts:
# requires:
# - build-npm-packages
# - publish_snapshot:
# # Note: no filters on this job because we want it to run for all upstream branches
# # We'd really like to filter out pull requests here, but not yet available:
# # https://discuss.circleci.com/t/workflows-pull-request-filter/14396/4
# # Instead, the job just exits immediately at the first step.
# requires:
# # Only publish if tests and integration tests pass
# - test
# - test_ivy_aot
# # Only publish if `aio`/`docs` tests using the locally built Angular packages pass
# - test_aio_local
# - test_aio_local_viewengine
# - test_docs_examples
# - test_docs_examples_viewengine
# # Get the artifacts to publish from the build-packages-dist job
# # since the publishing script expects the legacy outputs layout.
# - build-npm-packages
# - build-ivy-npm-packages
# - legacy-unit-tests-saucelabs
# # Temporarily disabled components-repo-unit-tests to update rules_nodejs to 2.0.0. Breaking changes in
# # rules_nodejs create a dependency sandwich between angular/angular & angular/components that are very
# # difficult and time consuming to resolve and involve patching @angular/bazel in components repo such
# # as https://github.com/angular/components/commit/9e7ba251207df77164d73d66620e619bcbc4d2ad. It is simpler to
# # 1) land angular/angular upgrade to rule_nodejs 2.0.0 which has breaking changes
# # 2) land angular/components upgrade to rules_nodejs 2.0.0 using the @angular/bazel builds snapshot
# # 3) update angular/angular to the landed components commit and re-enable these tests
# # - components-repo-unit-tests:
# # requires:
# # - build-npm-packages
# - test_zonejs:
# requires:
# - setup
# - test_win:
# requires:
# - setup
# - test_ivy_aot_win:
# requires:
# - setup
#
# monitoring:
# jobs:
# - setup
# - burn_rate:
# requires:
# - setup
# context:
# - burn-rate
# - aio_monitoring_stable:
# requires:
# - setup
# - aio_monitoring_next:
# requires:
# - setup
# - saucelabs_ivy:
# # Testing saucelabs via Bazel currently taking longer than the legacy saucelabs job as it
# # each karma_web_test target is provisioning and tearing down browsers which is adding
# # a lot of overhead. Running once daily on master only to avoid wasting resources and
# # slowing down CI for PRs.
# # TODO: Run this job on all branches (including PRs) once karma_web_test targets can
# # share provisioned browsers and we can remove the legacy saucelabs job.
# requires:
# - setup
# - saucelabs_view_engine:
# # Testing saucelabs via Bazel currently taking longer than the legacy saucelabs job as it
# # each karma_web_test target is provisioning and tearing down browsers which is adding
# # a lot of overhead. Running once daily on master only to avoid wasting resources and
# # slowing down CI for PRs.
# # TODO: Run this job on all branches (including PRs) once karma_web_test targets can
# # share provisioned browsers and we can remove the legacy saucelabs job.
# requires:
# - setup
# triggers:
# - schedule:
# <<: *only_on_master
# # Runs monitoring jobs at 10:00AM every day.
# cron: "0 10 * * *"
#
+1
View File
@@ -10,6 +10,7 @@ ts_library(
module_name = "@angular/dev-infra-private",
deps = [
"//dev-infra/caretaker",
"//dev-infra/ci",
"//dev-infra/commit-message",
"//dev-infra/format",
"//dev-infra/ngbot",
+45
View File
@@ -0,0 +1,45 @@
load("@npm//@bazel/typescript:index.bzl", "ts_library")
load("//tools:defaults.bzl", "jasmine_node_test")
ts_library(
name = "ci",
srcs = glob(
["**/*.ts"],
exclude = ["**/*.spec.ts"],
),
module_name = "@angular/dev-infra-private/ci",
visibility = ["//dev-infra:__subpackages__"],
deps = [
"//dev-infra/utils",
"@npm//@types/node",
"@npm//@types/node-fetch",
"@npm//@types/yargs",
"@npm//node-fetch",
"@npm//yargs",
],
)
ts_library(
name = "test_lib",
testonly = True,
srcs = glob(["**/*.spec.ts"]),
deps = [
":ci",
"//dev-infra/utils",
"//dev-infra/utils/testing",
"@npm//@types/jasmine",
"@npm//@types/node",
"@npm//@types/node-fetch",
"@npm//@types/semver",
"@npm//node-fetch",
"@npm//semver",
],
)
jasmine_node_test(
name = "test",
bootstrap = ["//tools/testing:node_no_angular_es5"],
deps = [
":test_lib",
],
)
+106
View File
@@ -0,0 +1,106 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import * as nodeFetch from 'node-fetch';
import * as utilsConfig from '../../utils/config'
import * as console from '../../utils/console';
import {BurnRateResults, checkBurnRate} from './burn-rate';
describe('burn rate check', () => {
let fetchSpy: jasmine.Spy;
let infoSpy: jasmine.Spy;
let debugSpy: jasmine.Spy;
let getConfigSpy: jasmine.Spy;
beforeEach(() => {
getConfigSpy =
spyOn(utilsConfig, 'getConfig').and.returnValue({github: {name: 'name', owner: 'owner'}});
fetchSpy = spyOn(nodeFetch, 'default');
infoSpy = spyOn(console, 'info');
debugSpy = spyOn(console, 'debug');
});
describe('retrieves the credit usage across a set of time windows', () => {
it('assumes Infinity credit usage when an error occurs on the API', async () => {
fetchSpy.and.callFake((url: string) => {
if (url.includes('last-90-days')) {
return buildFetchResponse({message: 'this is an error'});
}
return buildFetchResponse({items: [{metrics: {total_credits_used: 5}}]});
});
const rateResponse =
JSON.parse((await checkBurnRate({ciToken: 'token', json: true}))!) as BurnRateResults;
expect(rateResponse.one.credits).toBe(5);
expect(rateResponse.seven.credits).toBe(5);
expect(rateResponse.thirty.credits).toBe(5);
// null is expected because JSON.stringify converts Infinity to null.
expect(rateResponse.ninety.credits).toBe(null as any);
});
it('calculates monthly burn rates', async () => {
fetchSpy.and.returnValues(
buildFetchResponse({items: [{metrics: {total_credits_used: 123}}]}),
buildFetchResponse({items: [{metrics: {total_credits_used: 456}}]}),
buildFetchResponse({items: [{metrics: {total_credits_used: 789}}]}),
buildFetchResponse({items: [{metrics: {total_credits_used: 12345}}]}),
);
const rateResponse =
JSON.parse((await checkBurnRate({ciToken: 'token', json: true}))!) as BurnRateResults;
expect(rateResponse.one).toEqual({credits: 123, burnRate: 3741});
expect(rateResponse.seven).toEqual({credits: 456, burnRate: 1981});
expect(rateResponse.thirty).toEqual({credits: 789, burnRate: 800});
expect(rateResponse.ninety).toEqual({credits: 12345, burnRate: 4172});
});
});
describe('provides the results', () => {
it('as output in the terminal', async () => {
fetchSpy.and.returnValues(
buildFetchResponse({items: [{metrics: {total_credits_used: 123}}]}),
buildFetchResponse({items: [{metrics: {total_credits_used: 456}}]}),
buildFetchResponse({items: [{metrics: {total_credits_used: 789}}]}),
buildFetchResponse({items: [{metrics: {total_credits_used: 12345}}]}),
);
await checkBurnRate({ciToken: 'token', json: false});
expect(infoSpy).toHaveBeenCalledWith('Credits used in last 24 hours: 123 credits');
expect(infoSpy).toHaveBeenCalledWith('1 day: 3741/month (367.63%)');
expect(infoSpy).toHaveBeenCalledWith('7 day: 1981/month (147.63%)');
expect(infoSpy).toHaveBeenCalledWith('30 day: 800/month ---');
expect(infoSpy).toHaveBeenCalledWith('90 day: 4172/month (421.50%)');
});
it('as json output', async () => {
fetchSpy.and.returnValues(
buildFetchResponse({items: [{metrics: {total_credits_used: 123}}]}),
buildFetchResponse({items: [{metrics: {total_credits_used: 456}}]}),
buildFetchResponse({items: [{metrics: {total_credits_used: 789}}]}),
buildFetchResponse({items: [{metrics: {total_credits_used: 12345}}]}),
);
const output = await checkBurnRate({ciToken: 'token', json: true});
expect(output).toBe(JSON.stringify({
one: {credits: 123, burnRate: 3741},
seven: {credits: 456, burnRate: 1981},
thirty: {credits: 789, burnRate: 800},
ninety: {credits: 12345, burnRate: 4172},
}));
});
});
});
function buildFetchResponse(response: {[key: string]: any}) {
return Promise.resolve({json: () => Promise.resolve(response)});
}
+90
View File
@@ -0,0 +1,90 @@
import fetch from 'node-fetch';
import {getConfig} from '../../utils/config';
import {error, info, red, reset} from '../../utils/console';
/** Reporting windows for insight data from CircleCI. */
type ReportingWindow = 'last-90-days'|'last-30-days'|'last-7-days'|'last-24-hours';
/** API response structure from CircleCI's insight API. */
interface CircleCiInsightsResponse {
message?: string;
items: {
name: string; window_start: string; window_end: string; metrics: {
success_rate: number; total_runs: number; failed_runs: number; successful_runs: number;
throughput: number;
mttr: number;
total_credits_used: number;
duration_metrics: {
min: number; mean: number; median: number; p95: number; max: number;
standard_deviation: number;
}
}
}[];
next_page_token: string;
}
/** The Burn Rate check results as an object. */
export interface BurnRateResults {
one: {credits: number; burnRate: number;};
seven: {credits: number; burnRate: number;};
thirty: {credits: number; burnRate: number;};
ninety: {credits: number; burnRate: number;};
}
/** Check the burn rate of CircleCI credit usage for the repository. */
export async function checkBurnRate({ciToken, json}: {ciToken: string, json: boolean}) {
const usage = {
one: getUsageAndBurnRate(await getCreditsUsed(ciToken, 'last-24-hours'), 1),
seven: getUsageAndBurnRate(await getCreditsUsed(ciToken, 'last-7-days'), 7),
thirty: getUsageAndBurnRate(await getCreditsUsed(ciToken, 'last-30-days'), 30),
ninety: getUsageAndBurnRate(await getCreditsUsed(ciToken, 'last-90-days'), 90),
};
if (json === true) {
return JSON.stringify(usage);
}
info(`Credits used in last 24 hours: ${usage.one.credits} credits`);
info();
info.group(`Burn rate comparison (% change from 30 day rate):`);
info(`1 day: ${usage.one.burnRate}/month (${compareRates(usage.one.burnRate)})`);
info(`7 day: ${usage.seven.burnRate}/month (${compareRates(usage.seven.burnRate)})`);
info(`30 day: ${usage.thirty.burnRate}/month ---`);
info(`90 day: ${usage.ninety.burnRate}/month (${compareRates(usage.ninety.burnRate)})`);
info.groupEnd();
/** Compare two provided burn rates to determine the percentage change from the base rate. */
function compareRates(rate: number, base = usage.thirty.burnRate) {
if (rate === base) {
return '---';
}
const percentage = ((rate - base) / base * 100);
const color = percentage < 0 ? red : reset;
return color(`${percentage.toFixed(2)}%`);
}
}
/** Determine the credit usage and monthly burn rate for provide credit usage. */
function getUsageAndBurnRate(credits: number, days: number) {
return {
credits,
// Formula: [credits] / [days of usage data] / [months in year] * [days in a year] /
burnRate: Math.round(credits / days / 12 * 365)
};
}
/** Retrieve the number of credits used for the project in a provided reporting window. */
async function getCreditsUsed(ciToken: string, window: ReportingWindow) {
const {owner, name} = getConfig().github;
const url = `https://circleci.com/api/v2/insights/gh/${owner}/${
name}/workflows?reporting-window=${window}&circle-token=${ciToken}`;
const data = (await fetch(url).then(r => r.json())) as CircleCiInsightsResponse;
if (data.message) {
error(`An error response was returned for the reporting window: ${window}`);
return Infinity;
}
return data.items.map(workflow => workflow.metrics.total_credits_used).reduce((a, b) => a + b);
}
+40
View File
@@ -0,0 +1,40 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {Arguments, Argv, CommandModule} from 'yargs';
import {checkBurnRate} from './burn-rate';
export interface BurnRateOptions {
ciToken: string;
json: boolean;
}
/** Builds the command. */
function builder(yargs: Argv) {
return yargs.option('ciToken', {type: 'string', demandOption: true}).option('json', {
type: 'boolean',
default: false,
});
}
/** Handles the command. */
async function handler(args: Arguments<BurnRateOptions>) {
await checkBurnRate(args);
}
/** yargs command module describing the command. */
export const BurnRateModule: CommandModule<{}, BurnRateOptions> = {
handler,
builder,
command: 'check-burn-rate',
// Set describe to false to prevent the command from appearing in help menus as it is a hidden
// command.
describe: false,
};
+15
View File
@@ -0,0 +1,15 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import * as yargs from 'yargs';
import {BurnRateModule} from './burn-rate/cli';
/** Build the parser for the ci commands. */
export function buildCiParster(localYargs: yargs.Argv) {
return localYargs.help().strict().command(BurnRateModule);
}
+2
View File
@@ -17,6 +17,7 @@ import {buildPullapproveParser} from './pullapprove/cli';
import {buildReleaseParser} from './release/cli';
import {tsCircularDependenciesBuilder} from './ts-circular-dependencies/index';
import {captureLogOutputForCommand} from './utils/console';
import {buildCiParster} from './ci/cli';
yargs.scriptName('ng-dev')
.middleware(captureLogOutputForCommand)
@@ -30,6 +31,7 @@ yargs.scriptName('ng-dev')
.command('ts-circular-deps <command>', '', tsCircularDependenciesBuilder)
.command('caretaker <command>', '', buildCaretakerParser)
.command('ngbot <command>', false, buildNgbotParser)
.command('ci <command>', false, buildCiParster)
.wrap(120)
.strict()
.parse();
+90
View File
@@ -213,6 +213,7 @@ var green = chalk.green;
var yellow = chalk.yellow;
var bold = chalk.bold;
var blue = chalk.blue;
var reset = chalk.reset;
/** Prompts the user with a confirmation question and a specified message. */
function promptConfirm(message, defaultValue) {
if (defaultValue === void 0) { defaultValue = false; }
@@ -6933,6 +6934,94 @@ function convertReferenceChainToString(chain) {
return chain.join(' → ');
}
/** Check the burn rate of CircleCI credit usage for the repository. */
function checkBurnRate({ ciToken, json }) {
return tslib.__awaiter(this, void 0, void 0, function* () {
const usage = {
one: getUsageAndBurnRate(yield getCreditsUsed(ciToken, 'last-24-hours'), 1),
seven: getUsageAndBurnRate(yield getCreditsUsed(ciToken, 'last-7-days'), 7),
thirty: getUsageAndBurnRate(yield getCreditsUsed(ciToken, 'last-30-days'), 30),
ninety: getUsageAndBurnRate(yield getCreditsUsed(ciToken, 'last-90-days'), 90),
};
if (json === true) {
return JSON.stringify(usage);
}
info(`Credits used in last 24 hours: ${usage.one.credits} credits`);
info();
info.group(`Burn rate comparison (% change from 30 day rate):`);
info(`1 day: ${usage.one.burnRate}/month (${compareRates(usage.one.burnRate)})`);
info(`7 day: ${usage.seven.burnRate}/month (${compareRates(usage.seven.burnRate)})`);
info(`30 day: ${usage.thirty.burnRate}/month ---`);
info(`90 day: ${usage.ninety.burnRate}/month (${compareRates(usage.ninety.burnRate)})`);
info.groupEnd();
/** Compare two provided burn rates to determine the percentage change from the base rate. */
function compareRates(rate, base = usage.thirty.burnRate) {
if (rate === base) {
return '---';
}
const percentage = ((rate - base) / base * 100);
const color = percentage < 0 ? red : reset;
return color(`${percentage.toFixed(2)}%`);
}
});
}
/** Determine the credit usage and monthly burn rate for provide credit usage. */
function getUsageAndBurnRate(credits, days) {
return {
credits,
// Formula: [credits] / [days of usage data] / [months in year] * [days in a year] /
burnRate: Math.round(credits / days / 12 * 365)
};
}
/** Retrieve the number of credits used for the project in a provided reporting window. */
function getCreditsUsed(ciToken, window) {
return tslib.__awaiter(this, void 0, void 0, function* () {
const { owner, name } = getConfig().github;
const url = `https://circleci.com/api/v2/insights/gh/${owner}/${name}/workflows?reporting-window=${window}&circle-token=${ciToken}`;
const data = (yield fetch(url).then(r => r.json()));
if (data.message) {
error(`An error response was returned for the reporting window: ${window}`);
return Infinity;
}
return data.items.map(workflow => workflow.metrics.total_credits_used).reduce((a, b) => a + b);
});
}
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
/** Builds the command. */
function builder$a(yargs) {
return yargs.option('ciToken', { type: 'string', demandOption: true }).option('json', {
type: 'boolean',
default: false,
});
}
/** Handles the command. */
function handler$a(args) {
return tslib.__awaiter(this, void 0, void 0, function* () {
yield checkBurnRate(args);
});
}
/** yargs command module describing the command. */
const BurnRateModule = {
handler: handler$a,
builder: builder$a,
command: 'check-burn-rate',
// Set describe to false to prevent the command from appearing in help menus as it is a hidden
// command.
describe: false,
};
/** Build the parser for the ci commands. */
function buildCiParster(localYargs) {
return localYargs.help().strict().command(BurnRateModule);
}
yargs.scriptName('ng-dev')
.middleware(captureLogOutputForCommand)
.demandCommand()
@@ -6945,6 +7034,7 @@ yargs.scriptName('ng-dev')
.command('ts-circular-deps <command>', '', tsCircularDependenciesBuilder)
.command('caretaker <command>', '', buildCaretakerParser)
.command('ngbot <command>', false, buildNgbotParser)
.command('ci <command>', false, buildCiParster)
.wrap(120)
.strict()
.parse();
+1
View File
@@ -21,6 +21,7 @@ export const green: typeof chalk = chalk.green;
export const yellow: typeof chalk = chalk.yellow;
export const bold: typeof chalk = chalk.bold;
export const blue: typeof chalk = chalk.blue;
export const reset: typeof chalk = chalk.reset;
/** Prompts the user with a confirmation question and a specified message. */
export async function promptConfirm(message: string, defaultValue = false): Promise<boolean> {