mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
Compare commits
3 Commits
main
...
watch-burn-rates
| Author | SHA1 | Date | |
|---|---|---|---|
| cbe11f92ec | |||
| 16b6cac4ed | |||
| a4d3fadd31 |
+154
-122
@@ -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 * * *"
|
||||
#
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
],
|
||||
)
|
||||
@@ -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)});
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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> {
|
||||
|
||||
Reference in New Issue
Block a user