build: remove dev-infra directory and migrate to relying on @angular/dev-infra-private-builds (#43175)

Remove the dev-infra directory and complete the migration to using the code generated by the
angular/dev-infra repository.

PR Close #43175
This commit is contained in:
Joey Perrott
2021-08-05 16:56:46 -07:00
committed by Dylan Hunn
parent 768a6698a0
commit fe6a5acd17
323 changed files with 526 additions and 26536 deletions
+5 -5
View File
@@ -114,11 +114,11 @@ build:remote --cpu=k8
build:remote --host_cpu=k8
# Toolchain and platform related flags
build:remote --crosstool_top=//dev-infra/bazel/remote-execution/cpp:cc_toolchain_suite
build:remote --extra_toolchains=//dev-infra/bazel/remote-execution/cpp:cc_toolchain
build:remote --extra_execution_platforms=//dev-infra/bazel/remote-execution:platform
build:remote --host_platform=//dev-infra/bazel/remote-execution:platform
build:remote --platforms=//dev-infra/bazel/remote-execution:platform
build:remote --crosstool_top=@npm//@angular/dev-infra-private/bazel/remote-execution/cpp:cc_toolchain_suite
build:remote --extra_toolchains=@npm//@angular/dev-infra-private/bazel/remote-execution/cpp:cc_toolchain
build:remote --extra_execution_platforms=@npm//@angular/dev-infra-private/bazel/remote-execution:platform
build:remote --host_platform=@npm//@angular/dev-infra-private/bazel/remote-execution:platform
build:remote --platforms=@npm//@angular/dev-infra-private/bazel/remote-execution:platform
# Remote instance and caching
build:remote --remote_instance_name=projects/internal-200822/instances/primary_instance
-1
View File
@@ -38,7 +38,6 @@ merge:
- 'modules/benchmarks/**'
- 'modules/system.d.ts'
- 'packages/**'
- 'dev-infra/benchmark/driver-utilities/**'
# list of patterns to ignore for the files changed by the PR
exclude:
- 'packages/*'
+1 -2
View File
@@ -106,8 +106,7 @@ Fixes #<issue number>
# │ └─⫸ Commit Scope: animations|bazel|benchpress|common|compiler|compiler-cli|core|
# │ elements|forms|http|language-service|localize|platform-browser|
# │ platform-browser-dynamic|platform-server|router|service-worker|
# │ upgrade|zone.js|packaging|changelog|dev-infra|docs-infra|migrations|
# │ ngcc|ve
# │ upgrade|zone.js|packaging|changelog|docs-infra|migrations|ngcc|ve
# │ https://github.com/angular/angular/blob/master/CONTRIBUTING.md#scope
# │
# └─⫸ Commit Type: build|ci|docs|feat|fix|perf|refactor|style|test
+1 -1
View File
@@ -1,4 +1,4 @@
import {CaretakerConfig} from '../dev-infra/caretaker/config';
import {CaretakerConfig} from '@angular/dev-infra-private/ng-dev/caretaker/config';
/** The configuration for `ng-dev caretaker` commands. */
export const caretaker: CaretakerConfig = {
+1 -1
View File
@@ -1,4 +1,4 @@
import {CommitMessageConfig} from '../dev-infra/commit-message/config';
import {CommitMessageConfig} from '@angular/dev-infra-private/ng-dev/commit-message/config';
/**
* The configuration for `ng-dev commit-message` commands.
+1 -1
View File
@@ -1,4 +1,4 @@
import {FormatConfig} from '../dev-infra/format/config';
import {FormatConfig} from '@angular/dev-infra-private/ng-dev/format/config';
/**
* Configuration for the `ng-dev format` command.
+1 -1
View File
@@ -1,4 +1,4 @@
import {GithubConfig} from '../dev-infra/utils/config';
import {GithubConfig} from '@angular/dev-infra-private/ng-dev/utils/config';
/**
* Github configuration for the `ng-dev` command. This repository is used as
+2 -2
View File
@@ -1,5 +1,5 @@
import {DevInfraMergeConfig} from '../dev-infra/pr/merge/config';
import {getDefaultTargetLabelConfiguration} from '../dev-infra/pr/merge/defaults';
import {DevInfraMergeConfig} from '@angular/dev-infra-private/ng-dev/pr/merge/config';
import {getDefaultTargetLabelConfiguration} from '@angular/dev-infra-private/ng-dev/pr/merge/defaults';
import {github} from './github';
import {release} from './release';
+1 -1
View File
@@ -1,5 +1,5 @@
import {ReleaseConfig} from '@angular/dev-infra-private/ng-dev/release/config';
import {join} from 'path';
import {ReleaseConfig} from '../dev-infra/release/config';
/** Configuration for the `ng-dev release` command. */
export const release: ReleaseConfig = {
-1
View File
@@ -1155,7 +1155,6 @@ groups:
'.ng-dev/**',
'.vscode/**',
'.yarn/**',
'dev-infra/**',
'docs/*.md',
'docs/images/**',
'goldens/*',
+2 -3
View File
@@ -248,8 +248,7 @@ Any line of the commit message cannot be longer than 100 characters.
│ └─⫸ Commit Scope: animations|bazel|benchpress|common|compiler|compiler-cli|core|
│ elements|forms|http|language-service|localize|platform-browser|
│ platform-browser-dynamic|platform-server|router|service-worker|
│ upgrade|zone.js|packaging|changelog|dev-infra|docs-infra|migrations|
│ ngcc|ve
│ upgrade|zone.js|packaging|changelog|docs-infra|migrations|ngcc|ve
│
└─⫸ Commit Type: build|ci|docs|feat|fix|perf|refactor|test
```
@@ -302,7 +301,7 @@ There are currently a few exceptions to the "use package name" rule:
* `changelog`: used for updating the release notes in CHANGELOG.md
* `dev-infra`: used for dev-infra related changes within the directories /scripts, /tools and /dev-infra
* `dev-infra`: used for dev-infra related changes within the directories /scripts and /tools
* `docs-infra`: used for docs-app (angular.io) related changes within the /aio directory of the repo
+1 -1
View File
@@ -44,7 +44,7 @@ load("@io_bazel_rules_webtesting//web:repositories.bzl", "web_test_repositories"
web_test_repositories()
load("//dev-infra/bazel/browsers:browser_repositories.bzl", "browser_repositories")
load("@npm//@angular/dev-infra-private/bazel/browsers:browser_repositories.bzl", "browser_repositories")
browser_repositories()
-116
View File
@@ -1,116 +0,0 @@
# BEGIN-INTERNAL
load("@build_bazel_rules_nodejs//:index.bzl", "pkg_npm")
load("//dev-infra:defaults.bzl", "ng_dev_rolled_up_generated_file", "ts_library")
# END-INTERNAL
exports_files(["tsconfig.json"])
# BEGIN-INTERNAL
ts_library(
name = "cli",
srcs = [
"cli.ts",
],
deps = [
"//dev-infra/caretaker",
"//dev-infra/commit-message",
"//dev-infra/format",
"//dev-infra/misc",
"//dev-infra/ngbot",
"//dev-infra/pr",
"//dev-infra/pullapprove",
"//dev-infra/release",
"//dev-infra/ts-circular-dependencies",
"//dev-infra/utils",
"@npm//@types/node",
"@npm//@types/yargs",
"@npm//yargs",
],
)
genrule(
name = "package-json",
srcs = [
"tmpl-package.json",
"//:package.json",
],
outs = ["package.json"],
cmd = """
$(execpath //tools:inline-package-json-deps) $(execpath tmpl-package.json) \
$(execpath //:package.json) $@
""",
tools = ["//tools:inline-package-json-deps"],
)
pkg_npm(
name = "npm_package",
srcs = [
# Main bazel entry-point for the shared `dev-infra` package.
"index.bzl",
"BUILD.bazel",
# Some tools within `dev-infra` which are shipped as Bazel rules might
# rely on a tsconfig file. We bring the config into the NPM package.
"tsconfig.json",
"//dev-infra/bazel:files",
"//dev-infra/benchmark:files",
],
substitutions = {
# angular/angular should not consume it's own packages, so we use
# substitutions to replace these in the published version of dev-infra.
"@angular//dev-infra/": "@npm//@angular/dev-infra-private/",
"//dev-infra/": "@npm//@angular/dev-infra-private/",
"//dev-infra:": "@npm//@angular/dev-infra-private:",
# Substitutions needed for `//dev-infra/benchmark`:
"//packages/benchpress": "@npm//@angular/benchpress",
"//packages/bazel": "@npm//@angular/bazel",
"//packages/zone.js/bundles:zone.umd.js": "@npm//zone.js",
"//packages/core": "@npm//@angular/core",
"//packages/platform-browser": "@npm//@angular/platform-browser",
# This substitution is particularly verbose because we need to make sure
# that only things available via Angular Bazel are imported from
# tools/defaults.bzl.
"load\\(\"//tools:defaults.bzl\", \"ng_module\"\\)": "load(\"@npm//@angular/bazel:index.bzl\", \"ng_module\")",
},
visibility = ["//visibility:public"],
deps = [
":cli",
":package-json",
"//dev-infra/benchmark/driver-utilities",
"//dev-infra/commit-message",
"//dev-infra/ts-circular-dependencies",
"//dev-infra/tslint-rules",
],
)
# Because the angular/angular repository relies on the local repository for running ng-dev commands,
# the rollup generated javascript files are committed into the repository to be used as node
# scripts. To ensure they stay up to date, they are created using a generated file test.
#
# Currently there are two generated files which are needed
# ng-dev.js - The main script representing ng-dev
# build-worker.js - The worker script for the `ng-dev release build` command, allowing it to run
# in a forked process.
ng_dev_rolled_up_generated_file(
name = "ng-dev",
entry_point = ":cli.ts",
rollup_args = [
"--plugin",
"rollup-plugin-hashbang",
],
deps = [
":cli",
# TODO(josephperrott): Determine if this plugin is the best method for ensuring the hashbang
# in both local and published use case.
"@npm//rollup-plugin-hashbang",
],
)
ng_dev_rolled_up_generated_file(
name = "build-worker",
entry_point = "//dev-infra/release/build:build-worker.ts",
deps = [
"//dev-infra/release/build",
],
)
# END-INTERNAL
-13
View File
@@ -1,13 +0,0 @@
package(default_visibility = ["//visibility:public"])
filegroup(
name = "files",
srcs = [
"BUILD.bazel",
"expand_template.bzl",
"extract_js_module_output.bzl",
"//dev-infra/bazel/api-golden:files",
"//dev-infra/bazel/browsers:files",
"//dev-infra/bazel/remote-execution:files",
],
)
-35
View File
@@ -1,35 +0,0 @@
load("@npm//@bazel/typescript:index.bzl", "ts_library")
package(default_visibility = ["//visibility:public"])
exports_files([
"index.ts",
"index_npm_packages.ts",
])
ts_library(
name = "api-golden",
srcs = [
"find_entry_points.ts",
"index.ts",
"index_npm_packages.ts",
"test_api_report.ts",
],
# A tsconfig needs to be specified as otherwise `ts_library` will look for the config
# in `//:package.json` and this breaks when the BUILD file is copied to `@npm//`.
tsconfig = "//dev-infra:tsconfig.json",
deps = [
"@npm//@bazel/runfiles",
"@npm//@microsoft/api-extractor",
"@npm//@types/node",
"@npm//chalk",
"@npm//tslib",
"@npm//typescript",
],
)
# Expose the sources in the dev-infra NPM package.
filegroup(
name = "files",
srcs = glob(["*"]),
)
@@ -1,62 +0,0 @@
/**
* @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 {lstatSync, readdirSync, readFileSync} from 'fs';
import {dirname, join} from 'path';
/** Interface describing a resolved NPM package entry point. */
export interface PackageEntryPoint {
typesEntryPointPath: string;
packageJsonPath: string;
}
/** Interface describing contents of a `package.json`. */
interface PackageJson {
types?: string;
typings?: string;
}
/** Finds all entry points within a given NPM package directory. */
export function findEntryPointsWithinNpmPackage(dirPath: string): PackageEntryPoint[] {
const entryPoints: PackageEntryPoint[] = [];
for (const packageJsonFilePath of findPackageJsonFilesInDirectory(dirPath)) {
const packageJson = JSON.parse(readFileSync(packageJsonFilePath, 'utf8')) as PackageJson;
const typesFile = packageJson.types || packageJson.typings;
if (typesFile) {
entryPoints.push({
packageJsonPath: packageJsonFilePath,
typesEntryPointPath: join(dirname(packageJsonFilePath), typesFile),
});
}
}
return entryPoints;
}
/** Determine if the provided path is a directory. */
function isDirectory(dirPath: string) {
try {
return lstatSync(dirPath).isDirectory();
} catch {
return false;
}
}
/** Finds all `package.json` files within a directory. */
function* findPackageJsonFilesInDirectory(directoryPath: string): IterableIterator<string> {
for (const fileName of readdirSync(directoryPath)) {
const fullPath = join(directoryPath, fileName);
if (isDirectory(fullPath)) {
yield* findPackageJsonFilesInDirectory(fullPath);
} else if (fileName === 'package.json') {
yield fullPath;
}
}
}
-127
View File
@@ -1,127 +0,0 @@
load("//dev-infra/bazel:extract_js_module_output.bzl", "extract_js_module_output")
load("@build_bazel_rules_nodejs//:index.bzl", "nodejs_binary", "nodejs_test")
nodejs_test_args = [
# Needed so that node doesn't walk back to the source directory.
# From there, the relative imports would point to .ts files.
"--node_options=--preserve-symlinks",
# TODO(josephperrott): update dependency usages to no longer need bazel patch module resolver
# See: https://github.com/bazelbuild/rules_nodejs/wiki#--bazel_patch_module_resolver-now-defaults-to-false-2324
"--bazel_patch_module_resolver",
]
default_strip_export_pattern = "^ɵ(?!ɵdefineInjectable|ɵinject|ɵInjectableDef)"
"""Escapes a Regular expression so that it can be passed as process argument."""
def _escape_regex_for_arg(value):
return "\"%s\"" % value
"""
Extracts type names from a list of NPM type targets.
For example: Consider the `@npm//@types/node` target. This function extracts `node`
from the label. This is needed so that the Node types can be wired up within a
TypeScript program using the `types` tsconfig option.
"""
def extract_type_names_from_labels(type_targets):
type_names = []
for type_target in type_targets:
type_package = Label(type_target).package
if (type_package.startswith("@types/")):
type_names.append(type_package[len("@types/"):])
else:
fail("Expected type target to match the following format: " +
"`@<npm_workspace>//@types/<name>`, but got: %s" % type_target)
return type_names
"""
Builds an API report for the specified entry-point and compares it against the
specified golden
"""
def api_golden_test(
name,
golden,
entry_point,
data = [],
strip_export_pattern = default_strip_export_pattern,
types = [],
**kwargs):
quoted_export_pattern = _escape_regex_for_arg(strip_export_pattern)
kwargs["tags"] = kwargs.get("tags", []) + ["api_guard"]
# For API golden tests not running against a NPM package, we extract all transitive
# declarations of the specified `data` targets. This is necessary because API extractor
# needs to resolve other targets that have been linked by the Bazel NodeJS rules. The
# linker by default only provides access to JavaScript sources, but the API extractor is
# specifically concerned with type definitions that we can extract manually here.
extract_js_module_output(
name = "%s_data_typings" % name,
deps = data,
provider = "JSModuleInfo",
include_declarations = True,
include_default_files = False,
)
test_data = ["//dev-infra/bazel/api-golden", "//:package.json", ":%s_data_typings" % name] + \
data + types
nodejs_test(
name = name,
data = test_data,
entry_point = "//dev-infra/bazel/api-golden:index.ts",
templated_args = nodejs_test_args + [golden, entry_point, "false", quoted_export_pattern] +
extract_type_names_from_labels(types),
**kwargs
)
nodejs_binary(
name = name + ".accept",
testonly = True,
data = test_data,
entry_point = "//dev-infra/bazel/api-golden:index.ts",
templated_args = nodejs_test_args + [golden, entry_point, "true", quoted_export_pattern] +
extract_type_names_from_labels(types),
**kwargs
)
"""
Builds an API report for all entrypoints within the given NPM package and compares it
against goldens within the specified directory.
"""
def api_golden_test_npm_package(
name,
golden_dir,
npm_package,
data = [],
strip_export_pattern = default_strip_export_pattern,
types = [],
**kwargs):
quoted_export_pattern = _escape_regex_for_arg(strip_export_pattern)
kwargs["tags"] = kwargs.get("tags", []) + ["api_guard"]
nodejs_test(
name = name,
data = ["//dev-infra/bazel/api-golden"] + data + types,
entry_point = "//dev-infra/bazel/api-golden:index_npm_packages.ts",
templated_args = nodejs_test_args + [golden_dir, npm_package, "false", quoted_export_pattern] +
extract_type_names_from_labels(types),
**kwargs
)
nodejs_binary(
name = name + ".accept",
testonly = True,
data = ["//dev-infra/bazel/api-golden"] + data + types,
entry_point = "//dev-infra/bazel/api-golden:index_npm_packages.ts",
templated_args = nodejs_test_args + [golden_dir, npm_package, "true", quoted_export_pattern] +
extract_type_names_from_labels(types),
**kwargs
)
-47
View File
@@ -1,47 +0,0 @@
/**
* @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 {runfiles} from '@bazel/runfiles';
import * as chalk from 'chalk';
import {testApiGolden} from './test_api_report';
/**
* Entry point for the `api_golden_test` Bazel rule. This function builds an API report for
* the specified entry point file and compares it against the specified golden file.
*/
async function main(
goldenFilePath: string, entryPointFilePath: string, approveGolden: boolean,
stripExportPattern: RegExp, typeNames: string[]) {
const {succeeded, apiReportChanged} = await testApiGolden(
goldenFilePath, entryPointFilePath, approveGolden, stripExportPattern, typeNames);
if (!succeeded && apiReportChanged) {
console.error(chalk.red(`The API signature has changed and the golden file is outdated.`));
console.info(chalk.yellow(
`Golden can be updated by running: yarn bazel run ${process.env.TEST_TARGET}.accept`));
}
// Bazel expects `3` as exit code for failing tests.
process.exitCode = succeeded ? 0 : 3;
}
if (require.main === module) {
const args = process.argv.slice(2);
const goldenFilePath = runfiles.resolve(args[0]);
const entryPointFilePath = runfiles.resolve(args[1]);
const approveGolden = args[2] === 'true';
const stripExportPattern = new RegExp(args[3]);
const typeNames = args.slice(4);
main(goldenFilePath, entryPointFilePath, approveGolden, stripExportPattern, typeNames)
.catch(e => {
console.error(e);
process.exit(1);
});
}
@@ -1,74 +0,0 @@
/**
* @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 {runfiles} from '@bazel/runfiles';
import * as chalk from 'chalk';
import {join, relative} from 'path';
import {findEntryPointsWithinNpmPackage} from './find_entry_points';
import {testApiGolden} from './test_api_report';
/**
* Entry point for the `api_golden_test_npm_package` Bazel rule. This function determines
* all types within the specified NPM package and builds API reports that will be compared
* against golden files within the given golden directory.
*/
async function main(
goldenDir: string, npmPackageDir: string, approveGolden: boolean, stripExportPattern: RegExp,
typeNames: string[]) {
const entryPoints = findEntryPointsWithinNpmPackage(npmPackageDir);
const outdatedGoldens: string[] = [];
let allTestsSucceeding = true;
for (const {packageJsonPath, typesEntryPointPath} of entryPoints) {
const pkgRelativeName = relative(npmPackageDir, typesEntryPointPath);
// API extractor generates API reports as markdown files. For each types
// entry-point we maintain a separate golden file. These golden files are
// based on the name of the entry-point `.d.ts` file in the NPM package,
// but with the proper `.md` file extension.
// See: https://api-extractor.com/pages/overview/demo_api_report/.
const goldenName = pkgRelativeName.replace(/\.d\.ts$/, '.md');
const goldenFilePath = join(goldenDir, goldenName);
const {succeeded, apiReportChanged} = await testApiGolden(
goldenFilePath, typesEntryPointPath, approveGolden, stripExportPattern, typeNames,
packageJsonPath);
// Keep track of outdated goldens.
if (!succeeded && apiReportChanged) {
outdatedGoldens.push(goldenName);
}
allTestsSucceeding = allTestsSucceeding && succeeded;
}
if (outdatedGoldens.length) {
console.error(chalk.red(`The following goldens are outdated:`));
outdatedGoldens.forEach(name => console.info(`- ${name}`));
console.info();
console.info(chalk.yellow(
`The goldens can be updated by running: yarn bazel run ${process.env.TEST_TARGET}.accept`));
}
// Bazel expects `3` as exit code for failing tests.
process.exitCode = allTestsSucceeding ? 0 : 3;
}
if (require.main === module) {
const args = process.argv.slice(2);
const goldenDir = runfiles.resolve(args[0]);
const npmPackageDir = runfiles.resolve(args[1]);
const approveGolden = args[2] === 'true';
const stripExportPattern = new RegExp(args[3]);
const typeNames = args.slice(4);
main(goldenDir, npmPackageDir, approveGolden, stripExportPattern, typeNames).catch(e => {
console.error(e);
process.exit(1);
});
}
@@ -1,142 +0,0 @@
/**
* @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 {runfiles} from '@bazel/runfiles';
import {ConsoleMessageId, Extractor, ExtractorConfig, ExtractorLogLevel, ExtractorMessage, ExtractorMessageId, ExtractorResult, IConfigFile} from '@microsoft/api-extractor';
import {AstModule} from '@microsoft/api-extractor/lib/analyzer/AstModule';
import {ExportAnalyzer} from '@microsoft/api-extractor/lib/analyzer/ExportAnalyzer';
import {basename, dirname} from 'path';
/**
* Original definition of the `ExportAnalyzer#fetchAstModuleExportInfo` method.
* We store the original function since we monkey-patch it later to account for
* specified strip export patterns.
* */
const _origFetchAstModuleExportInfo = ExportAnalyzer.prototype.fetchAstModuleExportInfo;
/**
* Builds an API report for the given entry-point file and compares
* it against a golden file.
*
* @param goldenFilePath Path to an API report file that is used as golden
* @param indexFilePath Entry point file that is analyzed to build the API report.
* @param approveGolden Whether the golden file should be updated.
* @param stripExportPattern Regular Expression that can be used to filter out exports
* from the API report.
* @param typeNames Name of types which should be included for analysis of the entry-point.
* Types are expected to exist within the default `node_modules/@types/` folder.
* @param packageJsonPath Optional path to a `package.json` file that contains the entry
* point. Note that the `package.json` is currently only used by `api-extractor` to determine
* the package name displayed within the API golden.
*/
export async function testApiGolden(
goldenFilePath: string, indexFilePath: string, approveGolden: boolean,
stripExportPattern: RegExp, typeNames: string[] = [],
packageJsonPath = resolveWorkspacePackageJsonPath()): Promise<ExtractorResult> {
// If no `TEST_TMPDIR` is defined, then this script runs using `bazel run`. We use
// the runfile directory as temporary directory for API extractor.
const tempDir = process.env.TEST_TMPDIR ?? process.cwd();
const configObject: IConfigFile = {
compiler: {
overrideTsconfig:
// We disable automatic `@types` resolution as this throws-off API reports
// when the API test is run outside sandbox. Instead we expect a list of
// hard-coded types that should be included. This works in non-sandbox too.
{files: [indexFilePath], compilerOptions: {types: typeNames, lib: ['esnext', 'dom']}}
},
projectFolder: dirname(packageJsonPath),
mainEntryPointFilePath: indexFilePath,
dtsRollup: {enabled: false},
docModel: {enabled: false},
apiReport: {
enabled: true,
reportFolder: dirname(goldenFilePath),
reportTempFolder: tempDir,
reportFileName: basename(goldenFilePath),
},
tsdocMetadata: {enabled: false},
newlineKind: 'lf',
messages: {
extractorMessageReporting: {
// If an export does not have a release tag (like `@public`), API extractor maps
// considers it still as `Public`. We hide the message for now given the Angular
// repositories do not follow the TSDoc standard. https://tsdoc.org/.
// TODO: Make this an error once TSDoc standard is followed in all projects.
[ExtractorMessageId.MissingReleaseTag]: {logLevel: ExtractorLogLevel.None},
},
},
};
// We read the specified `package.json` manually and build a package name that is
// compatible with the API extractor. This is a workaround for a bug in api-extractor.
// TODO remove once https://github.com/microsoft/rushstack/issues/2774 is resolved.
const packageJson = require(packageJsonPath);
const packageNameSegments = packageJson.name.split('/');
const packageName = packageNameSegments.length === 1 ?
packageNameSegments[0] :
`${packageNameSegments[0]}/${packageNameSegments.slice(1).join('_')}`;
const extractorConfig = ExtractorConfig.prepare({
configObject,
// TODO: Remove workaround once https://github.com/microsoft/rushstack/issues/2774 is fixed.
packageJson: {name: packageName},
packageJsonFullPath: packageJsonPath,
configObjectFullPath: undefined,
});
// This patches the `ExportAnalyzer` of `api-extractor` so that we can filter out
// exports that match a specified pattern. Ideally this would not be needed as the
// TSDoc JSDoc annotations could be used to filter out exports from the API report,
// but there are cases in Angular where exports cannot be `@internal` but at the same
// time are denoted as unstable. Such exports are allowed to change frequently and should
// not be captured in the API report (as this would be unnecessarily inconvenient).
ExportAnalyzer.prototype.fetchAstModuleExportInfo = function(module: AstModule) {
const info = _origFetchAstModuleExportInfo.apply(this, [module]);
info.exportedLocalEntities.forEach((entity, exportName) => {
if (stripExportPattern.test(exportName)) {
info.exportedLocalEntities.delete(exportName);
}
});
return info;
};
return Extractor.invoke(extractorConfig, {
// If the golden should be approved, then `localBuild: true` instructs
// API extractor to update the file.
localBuild: approveGolden,
// Process messages from the API extractor (and modify log levels if needed).
messageCallback: msg => processExtractorMessage(msg, approveGolden),
});
}
/**
* Process an API extractor message. Microsoft's API extractor allows developers to
* handle messages before API extractor prints them. This allows us to adjust log level
* for certain messages, or to fully prevent messages from being printed out.
* */
async function processExtractorMessage(message: ExtractorMessage, isApprove: boolean) {
// If the golden does not match, we hide the error as API extractor prints
// a warning asking the user to manually copy the new API report. We print
// a custom warning below asking the developer to run the `.accept` Bazel target.
// TODO: Simplify once https://github.com/microsoft/rushstack/issues/2773 is resolved.
if (message.messageId === ConsoleMessageId.ApiReportNotCopied) {
// Mark the message as handled so that API-extractor does not print it. We print
// a message manually after extraction.
message.handled = true;
message.logLevel = isApprove ? ExtractorLogLevel.None : ExtractorLogLevel.Error;
}
}
/** Resolves the `package.json` of the workspace executing this action. */
function resolveWorkspacePackageJsonPath(): string {
const workspaceName = process.env.BAZEL_WORKSPACE!;
return runfiles.resolve(`${workspaceName}/package.json`);
}
-10
View File
@@ -1,10 +0,0 @@
package(default_visibility = ["//visibility:public"])
# Make source files available for distribution via pkg_npm
filegroup(
name = "files",
srcs = glob(["*"]) + [
"//dev-infra/bazel/browsers/chromium:files",
"//dev-infra/bazel/browsers/firefox:files",
],
)
-110
View File
@@ -1,110 +0,0 @@
# Browser configuration and versioning for testing of Angular
Within the Angular organization, we use Chrome and Firefox to perform most of the local testing, and rely on Sauce Labs and BrowserStack to do cross-browser testing on our CI.
The version of Chrome used in tests within this monorepo is configured and controlled via Bazel and `puppeteer`.
We manually keep the configuration of these two tools in sync to create a consistent testing environment across unit, e2e, and integration tests.
## Bazel
Bazel `karma_web_test_suite` and `protractor_web_test_suite` targets will use Chromium or Firefox provisioned by `//dev-infra/bazel/browsers`.
The version of Chrome and Firefox are specified in the `chromium.bzl` and `firefox.bzl` files in `/dev-infra/bazel/browsers`.
The process of updating the Chrome or Firefox version is not straightforward, but below are dedicated sections for each browser.
## Updating Chromium
1. Visit https://chromium.woolyss.com/ and note the version (commit position) of the latest stable version.
For example, "Google Chrome 83.0.4103.97 (756066) • Wednesday, 3 Jun 2020".
Alternatively, you can look in https://omahaproxy.appspot.com/.
2. Find the closest commit position number available for each platform in chromium-browser-snapshots: https://commondatastorage.googleapis.com/chromium-browser-snapshots/index.html
For example:
* https://commondatastorage.googleapis.com/chromium-browser-snapshots/index.html?prefix=Linux_x64/756066/
* https://commondatastorage.googleapis.com/chromium-browser-snapshots/index.html?prefix=Mac/756053/
* https://commondatastorage.googleapis.com/chromium-browser-snapshots/index.html?prefix=Win/756065/
You can download Chromium for your local platform and double check that the `--version` matches up with what you expect.
For example:
``` bash
$ ~/Downloads/chrome-mac/Chromium.app/Contents/MacOS/Chromium --version
Chromium 83.0.4103.0
```
3. Update the chrome & chrome driver build numbers in `dev-infra/bazel/browsers/chromium/chromium.bzl` and either run `bazel query @org_chromium_chromium_amd64//...` to prompt Bazel to calculate the new `sha256` for each platform binary or determine the new `sha256` values manually.
Here is an example with `curl` & `shasum`:
``` bash
curl -L https://commondatastorage.googleapis.com/chromium-browser-snapshots/Linux_x64/756066/chrome-linux.zip | shasum -a 256
```
## Puppeteer
1. Visit https://github.com/puppeteer/puppeteer/blob/master/docs/api.md to determine which version of puppeteer corresponds to the version of Chrome desired.
2. Visit https://chromedriver.chromium.org/downloads to determine which version of ChromeDriver should be used for the version of Chrome desired.
> NOTE:
> The version of Chrome does not necessarily correspond exactly with the version of ChromeDriver.
> For example, you might have to use ChromeDriver v87.0.4280.x to drive Chrome v87.0.4272.x.
3. Update `scripts/puppeteer-chromedriver-versions.js` to include an entry with the new version of puppeteer as key and the new version of ChromeDriver as value (as determined in the two previous steps).
4. Update all of the puppeteer versions throughout the repo:
* `package.json`
* `aio/package.json`
* `aio/tools/examples/shared/package.json`
...and their corresponding `yarn.lock` files.
## Firefox
In order to update Firefox, open the `dev-infra/bazel/browsers/firefox/firefox.bzl` file and update the repository URLs to the desired version.
For example:
```bzl
platform_http_file(
name = "org_mozilla_firefox_amd64",
licenses = ["reciprocal"], # MPL 2.0
sha256 = "bde6e020556a21561e4b8d7aaecf8db7077951f179b98ca5d0305435bc6802c9",
# Firefox v78.0
urls = ["https://ftp.mozilla.org/pub/firefox/releases/78.0/linux-x86_64/en-US/firefox-78.0.tar.bz2"],
)
```
1. Go to the `urls` property and update the URL by replacing all `78.0` occurrences with the version you intend to use.
Once done, do the same change for other platforms (such as `macos`).
2. Update the `sha256` checksum of the browser archives.
You can do this by downloading the artifacts from the URLs you just updated, and then running `shasum` on those files:
```sh
curl -L <BROWSER_URL> | shasum -a 256
```
In the same file, you can also update the version of gecko driver (the WebDriver implementation for Firefox browsers).
1. Go to https://firefox-source-docs.mozilla.org/testing/geckodriver/Support.html and find a version that is compatible with the used version of Firefox.
2. Update the `geckodriver` repository URLs to the desired version:
```bzl
platform_http_file(
name = "org_mozilla_geckodriver_amd64",
licenses = ["reciprocal"], # MPL 2.0
sha256 = "d59ca434d8e41ec1e30dd7707b0c95171dd6d16056fb6db9c978449ad8b93cc0",
# Geckodriver v0.26.0
urls = ["https://github.com/mozilla/geckodriver/releases/download/v0.26.0/geckodriver-v0.26.0-linux64.tar.gz"],
)
```
For example, replace all occurrences of `0.26.0` with the newer version.
3. Update the `sha256` checksum of the driver archives.
You can do this by downloading the artifacts from the URLs you just updated, and then running `shasum` on those files:
```sh
curl -L <DRIVER_URL> | shasum -a 256
```
@@ -1,96 +0,0 @@
"""Implementation of the `browser_archive` rule."""
def _browser_archive_impl(ctx):
url = ctx.attr.url
sha256 = ctx.attr.sha256
# If the URL resolves to a `.dmg` file, then we need to convert the file
# to a zip so that we can extract the actual binaries. We use the `convert_dmg`
# script provided by the webtesting Bazel rules.
if url.endswith(".dmg"):
download_file_name = "_download_file_%s.dmg" % ctx.attr.name
result_zip_name = "_converted_file_%s.zip" % ctx.attr.name
ctx.download(url, download_file_name, sha256)
ctx.execute([ctx.path(Label("@io_bazel_rules_webtesting//web/internal:convert_dmg.sh")), download_file_name, result_zip_name])
ctx.extract(result_zip_name)
ctx.delete(result_zip_name)
ctx.delete(download_file_name)
else:
ctx.download_and_extract(
url = url,
sha256 = sha256,
)
# The browser archive has been downloaded and extracted. We now generate a repository
# `BUILD.bazel` file that exposes the archive files, together with the specified
# named files using the `browser_configure` rule.
ctx.file("BUILD.bazel", content = """
load("@angular//dev-infra/bazel/browsers:browser_configure.bzl", "browser_configure")
licenses(%s)
browser_configure(
name = "metadata",
files = glob(["**/*"]),
named_files = %s,
visibility = ["//visibility:public"],
)
""" % (str(ctx.attr.licenses), str(ctx.attr.named_files)))
"""
Rule that can be used to download and unpack a browser archive in a dedicated Bazel
repository. Additionally, files within the archive can be denoted with an unique name
so that web tests can access browser files in a platform-agnostic way, regardless of
which `browser_archive` repository is added as dependency.
As an example for the concept of denoting archive files with an unique name, consider a case
where a a web test decides conditionally based on the current exec platform which
`browser_archive` repository is used (e.g. mac, windows or linux). The archives are different
for each platform. The test usually would need to determine the current platform, and know how
each archive is structured in order to access the browser binary within the repository. By
defining named files though, the web test could just pull a named file called `BINARY` that
always resolves to the browser binary in a platform-agnostic way.
Note #1: This rule exists as an alternative to the `platform_http_file` concept
from `rules_webtesting` because the `platform_http_file` rule does not extract the archive
directly, but relies on later build actions to perform the unpacking. This results in less
efficient caching because build actions are invalidated more frequently (e.g. `bazel clean).
We also noticed that the extraction within RBE containers is rather unstable, and extracting
the archives as part of a Bazel repository mitigates this (as extractions happens on the host).
Note #2: Additionally `rules_webtesting` defines a single repository for all platforms,
where only an archive for the current host platform is pulled. This breaks cross-compilation
because the wrong platform archive would be used for web tests that run in the exec platform.
"""
browser_archive = repository_rule(
implementation = _browser_archive_impl,
attrs = {
"url": attr.string(
doc = "Browser archive to download and extract.",
mandatory = True,
),
"sha256": attr.string(
doc = "SHA256 checksum for the archive.",
mandatory = True,
),
"licenses": attr.string_list(
mandatory = True,
allow_empty = False,
doc = """
Licenses that apply to the archive. Will be passed to a `licenses` invocation
within the repository. https://docs.bazel.build/versions/0.24.0/be/functions.html#licenses.
""",
),
"named_files": attr.string_dict(
doc = """
Dictionary that maps files to unique identifiers. This is useful
if browser archives are different on different platforms and the web
tests would not want to care about archive-specific paths. e.g. targets
expect a `CHROMIUM` key to point to the Chromium browser binary.
""",
mandatory = True,
),
},
)
@@ -1,69 +0,0 @@
load("@io_bazel_rules_webtesting//web/internal:metadata.bzl", "metadata")
load("@io_bazel_rules_webtesting//web/internal:provider.bzl", "WebTestInfo")
"""Converts the specified label to a manifest path"""
def _label_to_manifest_path(label):
if label.package != "":
return "%s/%s" % (label.workspace_name, label.package)
return label.workspace_name
"""Implementation of the `browser_configure` rule."""
def _browser_configure_impl(ctx):
named_files = {}
base_dir = _label_to_manifest_path(ctx.label)
# Update the named files to manifest paths that can be resolved
# with Bazel runfile resolution in web tests.
for n, p in ctx.attr.named_files.items():
named_files[n] = base_dir + "/" + p
# Create a web test metadata file that will be provided as part of
# the `WebTestInfo` provider.
metadata.create_file(
ctx = ctx,
output = ctx.outputs.web_test_metadata,
web_test_files = [
metadata.web_test_files(ctx = ctx, named_files = named_files),
],
)
return [
DefaultInfo(runfiles = ctx.runfiles(files = ctx.files.files)),
WebTestInfo(metadata = ctx.outputs.web_test_metadata),
]
"""
Rule that is used in combination with the `browser_archive` rule. It captures a set
of files which are needed for dealing with a browser. Additionally, specific files
for the browser can be denoted with an unique name so that web tests can access browser
files in a platform-agnostic way, regardless of which browser repository is selected.
The specified browser files are exposed as runfiles of the target defined through this
rule. The unique names with their associated files are captured within a metadata file
that is exposed through a `WebTestInfo` provider. Web tests will be able to deal with
this metadata file to resolve browser files in a platform-agnostic way.
More details on this can be found in the `browser_archive` rule.
"""
browser_configure = rule(
attrs = {
"files": attr.label_list(
mandatory = True,
allow_files = True,
doc = "List of files which are needed for the browser.",
),
"named_files": attr.string_dict(
doc = """
Dictionary that maps files to unique identifiers. This is useful
if browser archives are different on different platforms and the web
tests would not want to care about archive-specific paths. e.g. targets
expect a `CHROMIUM` key to point to the Chromium browser binary.
""",
mandatory = True,
),
},
outputs = {"web_test_metadata": "%{name}.gen.json"},
implementation = _browser_configure_impl,
)
@@ -1,15 +0,0 @@
"""Pinned browser versions.
This function is here to make browser repositories work with cross-platform RBE.
Unlike the `rules_webtesting` `browser_repositories`, this function defines
separate repositories for each platform.
"""
load("//dev-infra/bazel/browsers/chromium:chromium.bzl", "define_chromium_repositories")
load("//dev-infra/bazel/browsers/firefox:firefox.bzl", "define_firefox_repositories")
def browser_repositories():
"""Load pinned rules_webtesting browser versions."""
define_chromium_repositories()
define_firefox_repositories()
@@ -1,30 +0,0 @@
load("@io_bazel_rules_webtesting//web:web.bzl", "browser")
package(default_visibility = ["//visibility:public"])
browser(
name = "chromium",
metadata = "chromium.json",
deps = [
"@io_bazel_rules_webtesting//go/wsl",
] + select({
"@io_bazel_rules_webtesting//common/conditions:linux": [
"@org_chromium_chromedriver_amd64//:metadata",
"@org_chromium_chromium_amd64//:metadata",
],
"@io_bazel_rules_webtesting//common/conditions:mac": [
"@org_chromium_chromedriver_macos//:metadata",
"@org_chromium_chromium_macos//:metadata",
],
"@io_bazel_rules_webtesting//common/conditions:windows": [
"@org_chromium_chromedriver_windows//:metadata",
"@org_chromium_chromium_windows//:metadata",
],
}),
)
# Make source files available for distribution via pkg_npm
filegroup(
name = "files",
srcs = glob(["*"]),
)
@@ -1,76 +0,0 @@
load("//dev-infra/bazel/browsers:browser_archive_repo.bzl", "browser_archive")
"""
Defines repositories for Chromium that can be used inside Karma unit tests
and Protractor e2e tests with Bazel.
"""
def define_chromium_repositories():
# To update to a newer version of Chromium see instructions in
# https://github.com/angular/angular/blob/master/dev-infra/bazel/browsers/README.md.
browser_archive(
name = "org_chromium_chromium_amd64",
licenses = ["notice"], # BSD 3-clause (maybe more?)
sha256 = "36759ed6d151645d00a3a015200334edc70188b422eec51bcaa5790c8e906e27",
# 87.0.4280
url = "https://commondatastorage.googleapis.com/chromium-browser-snapshots/Linux_x64/812847/chrome-linux.zip",
named_files = {
"CHROMIUM": "chrome-linux/chrome",
},
)
browser_archive(
name = "org_chromium_chromium_macos",
licenses = ["notice"], # BSD 3-clause (maybe more?)
sha256 = "e10533c84ef57232975d6bde9cd28fd0354371e9556dda85e01178e6dcd56b93",
# 87.0.4280
url = "https://commondatastorage.googleapis.com/chromium-browser-snapshots/Mac/812851/chrome-mac.zip",
named_files = {
"CHROMIUM": "chrome-mac/Chromium.app/Contents/MacOS/chromium",
},
)
browser_archive(
name = "org_chromium_chromium_windows",
licenses = ["notice"], # BSD 3-clause (maybe more?)
sha256 = "40d0dec1892d729db2f7d8f27feff762b070a02f04d4e14f4e37b97d6b7c3c8f",
# 87.0.4280
url = "https://commondatastorage.googleapis.com/chromium-browser-snapshots/Win/812822/chrome-win.zip",
named_files = {
"CHROMIUM": "chrome-win/chrome.exe",
},
)
browser_archive(
name = "org_chromium_chromedriver_amd64",
licenses = ["reciprocal"], # BSD 3-clause, ICU, MPL 1.1, libpng (BSD/MIT-like), Academic Free License v. 2.0, BSD 2-clause, MIT
sha256 = "d859f8ecb21e26d3ddaf3f229da695bc86512f4e6c9fe32533af7a8b36783ec5",
# 87.0.4280
url = "https://commondatastorage.googleapis.com/chromium-browser-snapshots/Linux_x64/812847/chromedriver_linux64.zip",
named_files = {
"CHROMEDRIVER": "chromedriver_linux64/chromedriver",
},
)
browser_archive(
name = "org_chromium_chromedriver_macos",
licenses = ["reciprocal"], # BSD 3-clause, ICU, MPL 1.1, libpng (BSD/MIT-like), Academic Free License v. 2.0, BSD 2-clause, MIT
sha256 = "aa7a99fa23287725d7108cc07baa94e6f0ef4171ff7b134018387a939a67d93d",
# 87.0.4280
url = "https://commondatastorage.googleapis.com/chromium-browser-snapshots/Mac/812851/chromedriver_mac64.zip",
named_files = {
"CHROMEDRIVER": "chromedriver_mac64/chromedriver",
},
)
browser_archive(
name = "org_chromium_chromedriver_windows",
licenses = ["reciprocal"], # BSD 3-clause, ICU, MPL 1.1, libpng (BSD/MIT-like), Academic Free License v. 2.0, BSD 2-clause, MIT
sha256 = "826f2bd0c50b823e7642860ed08cacf69d3756002a71ac30cdd77c68f31d2d24",
# 87.0.4280
url = "https://commondatastorage.googleapis.com/chromium-browser-snapshots/Win/812822/chromedriver_win32.zip",
named_files = {
"CHROMEDRIVER": "chromedriver_win32/chromedriver.exe",
},
)
@@ -1,20 +0,0 @@
{
"environment" : "local",
"capabilities" : {
"browserName" : "chrome",
"goog:chromeOptions" : {
"binary" : "%FILE:CHROMIUM%",
"args" : [
"--headless",
"--use-gl=swiftshader-webgl"
]
},
"google:wslConfig": {
"binary": "%FILE:CHROMEDRIVER%",
"port":"%WSLPORT:WSL%",
"args": ["--port=%WSLPORT:WSL%"],
"status": true,
"shutdown": true
}
}
}
@@ -1,33 +0,0 @@
load("@io_bazel_rules_webtesting//web:web.bzl", "browser")
package(default_visibility = ["//visibility:public"])
browser(
name = "firefox",
disabled = select({
# TODO: Consider adding support for Windows. Requires a portable version of
# Firefox. Official distribution only ships with installers.
"@io_bazel_rules_webtesting//common/conditions:windows": "Firefox is not supported on Windows",
"//conditions:default": None,
}),
metadata = "firefox.json",
deps = [
"@io_bazel_rules_webtesting//go/wsl",
] + select({
"@io_bazel_rules_webtesting//common/conditions:linux": [
"@org_mozilla_firefox_amd64//:metadata",
"@org_mozilla_geckodriver_amd64//:metadata",
],
"@io_bazel_rules_webtesting//common/conditions:mac": [
"@org_mozilla_firefox_macos//:metadata",
"@org_mozilla_geckodriver_macos//:metadata",
],
"@io_bazel_rules_webtesting//common/conditions:windows": [],
}),
)
# Make source files available for distribution via pkg_npm
filegroup(
name = "files",
srcs = glob(["*"]),
)
@@ -1,54 +0,0 @@
load("//dev-infra/bazel/browsers:browser_archive_repo.bzl", "browser_archive")
"""
Defines repositories for Firefox that can be used inside Karma unit tests
and Protractor e2e tests with Bazel.
"""
def define_firefox_repositories():
# Instructions on updating the Firefox version can be found in the `README.md` file
# next to this file.
browser_archive(
name = "org_mozilla_firefox_amd64",
licenses = ["reciprocal"], # MPL 2.0
sha256 = "601e5a9a12ce680ecd82177c7887dae008d8f33690da43be1a690b76563cd992",
# Firefox v84.0
url = "https://ftp.mozilla.org/pub/firefox/releases/84.0/linux-x86_64/en-US/firefox-84.0.tar.bz2",
named_files = {
"FIREFOX": "firefox/firefox",
},
)
browser_archive(
name = "org_mozilla_firefox_macos",
licenses = ["reciprocal"], # MPL 2.0
sha256 = "4c7bca050eb228f4f6f93a9895af0a87473e03c67401d1d2f1ba907faf87fefd",
# Firefox v84.0
url = "https://ftp.mozilla.org/pub/firefox/releases/84.0/mac/en-US/Firefox%2084.0.dmg",
named_files = {
"FIREFOX": "Firefox.app/Contents/MacOS/firefox",
},
)
browser_archive(
name = "org_mozilla_geckodriver_amd64",
licenses = ["reciprocal"], # MPL 2.0
sha256 = "61bfc547a623d7305256611a81ecd24e6bf9dac555529ed6baeafcf8160900da",
# Geckodriver v0.28.0
url = "https://github.com/mozilla/geckodriver/releases/download/v0.28.0/geckodriver-v0.28.0-linux64.tar.gz",
named_files = {
"GECKODRIVER": "geckodriver",
},
)
browser_archive(
name = "org_mozilla_geckodriver_macos",
licenses = ["reciprocal"], # MPL 2.0
sha256 = "c288ff6db39adfd5eea0e25b4c3e71bfd9fb383eccf521cdd65f67ea78eb1761",
# Geckodriver v0.28.0
url = "https://github.com/mozilla/geckodriver/releases/download/v0.28.0/geckodriver-v0.28.0-macos.tar.gz",
named_files = {
"GECKODRIVER": "geckodriver",
},
)
@@ -1,21 +0,0 @@
{
"environment": "local",
"capabilities": {
"browserName": "firefox",
"moz:firefoxOptions": {
"binary": "%FILE:FIREFOX%",
"args": ["--headless"]
},
"google:wslConfig": {
"binary": "%FILE:GECKODRIVER%",
"port":"%WSLPORT:WSL%",
"args": [
"--port=%WSLPORT:WSL%",
"--host=%WSL:HOST_IP%",
"--marionette-port=%WSLPORT:MARIONETTE%"
],
"status": true,
"shutdown": false
}
}
}
-45
View File
@@ -1,45 +0,0 @@
"""Implementation of the expand_template rule """
def expand_template_impl(ctx):
substitutions = dict()
for k in ctx.attr.configuration_env_vars:
if k in ctx.var.keys():
substitutions["TMPL_%s" % k] = ctx.var[k]
for k in ctx.attr.substitutions:
substitutions[k] = ctx.expand_location(ctx.attr.substitutions[k], targets = ctx.attr.data)
ctx.actions.expand_template(
template = ctx.file.template,
output = ctx.outputs.output_name,
substitutions = substitutions,
)
"""Rule that can be used to substitute variables in a given template file."""
expand_template = rule(
implementation = expand_template_impl,
attrs = {
"configuration_env_vars": attr.string_list(
default = [],
doc = "Bazel configuration variables which should be exposed to the template.",
),
"output_name": attr.output(
mandatory = True,
doc = "File where the substituted template is written to.",
),
"substitutions": attr.string_dict(
mandatory = True,
doc = "Dictionary of substitutions that should be available to the template. Dictionary key represents the placeholder in the template.",
),
"data": attr.label_list(
doc = """Data dependencies for location expansion.""",
allow_files = True,
),
"template": attr.label(
mandatory = True,
allow_single_file = True,
doc = "File used as template.",
),
},
)
@@ -1,78 +0,0 @@
load("@build_bazel_rules_nodejs//:providers.bzl", "DeclarationInfo", "JSEcmaScriptModuleInfo", "JSModuleInfo", "JSNamedModuleInfo")
"""Converts a provider name to its actually Starlark provider instance."""
def _name_to_js_module_provider(name):
if name == "JSModuleInfo":
return JSModuleInfo
elif name == "JSNamedModuleInfo":
return JSNamedModuleInfo
elif name == "JSEcmaScriptModuleInfo":
return JSEcmaScriptModuleInfo
fail("Unexpected JavaScript module provider.")
"""Implementation of the extract_js_module_output rule."""
def _extract_js_module_output_impl(ctx):
js_module_provider = _name_to_js_module_provider(ctx.attr.provider)
depsets = []
for dep in ctx.attr.deps:
# Include JavaScript sources (including transitive outputs) based on the
# configured JavaScript module provider.
if js_module_provider in dep:
depsets.append(dep[js_module_provider].sources)
# Based on whether declarations should be collected, extract direct
# and transitive declaration files using the `DeclarationInfo` provider.
if ctx.attr.include_declarations and DeclarationInfo in dep:
depsets.append(dep[DeclarationInfo].transitive_declarations)
# Based on whether default files should be collected, extract direct
# files which are exposed using the `DefaultInfo` provider. Also include
# data runfiles which are needed for the current target.
# https://docs.bazel.build/versions/main/skylark/lib/DefaultInfo.html#data_runfiles
if ctx.attr.include_default_files and DefaultInfo in dep:
depsets.append(dep[DefaultInfo].files)
depsets.append(dep[DefaultInfo].data_runfiles.files)
sources = depset(transitive = depsets)
return [DefaultInfo(files = sources)]
"""
Rule that collects declared JavaScript module output files from a list of dependencies
based on a configurable JavaScript module provider. The extracted outputs are exposed
within the `DefaultInfo` provider. Targets defined using this rule can be used as input
for rules that require JavaScript sources, or if there are multiple JavaScript output
variants defined for a target while for example only the `JSModule` outputs are of interest.
As an example: This rule is helpful in combination with `ts_library` and `ng_module` as
those rule expose multiple output flavors (which are distinguishable by the JavaScript module
providers as imported from `providers.bzl`). i.e. these rules expose flavors for named AMD
modules and ECMAScript module output. For reference:
https://github.com/bazelbuild/rules_nodejs/blob/stable/packages/typescript/internal/build_defs.bzl#L334-L337
"""
extract_js_module_output = rule(
implementation = _extract_js_module_output_impl,
attrs = {
"deps": attr.label_list(
allow_files = True,
),
"provider": attr.string(
doc = "JavaScript module info provider that is used for collecting sources from the dependencies.",
mandatory = True,
values = ["JSModuleInfo", "JSNamedModuleInfo", "JSEcmaScriptModuleInfo"],
),
"include_declarations": attr.bool(
mandatory = True,
doc = "Whether declaration files should be collected from the dependencies.",
),
"include_default_files": attr.bool(
mandatory = True,
doc = """
Whether files from the `DefaultInfo` provider should be collected. Includes
data runfiles needed for the default outputs from dependencies.
""",
),
},
)
@@ -1,36 +0,0 @@
package(default_visibility = ["//visibility:public"])
platform(
name = "platform",
constraint_values = [
"@bazel_tools//platforms:linux",
"@bazel_tools//platforms:x86_64",
"@bazel_tools//tools/cpp:clang",
],
exec_properties = {
# We use a basic docker image from the Google Cloud container registry that supports
# browser tests. Note that we usually do not use any of the local browsers, but the image
# guarantees that necessary dependencies for launching browsers are installed. Since we
# do not rely on many binaries/tools from the image, the image doesn't need to be updated
# frequently. There are rare cases where it needs to be updated. e.g. for a more recent Bash
# version, or new system settings that are required for launching browsers. In order to do that,
# we need to either see if the `rbe-ubuntu16-04-webtest` image can be updated, or if we need to
# build and publish our own image to the Google cloud image registry.
"container-image": "docker://gcr.io/cloud-marketplace/google/rbe-ubuntu16-04-webtest@sha256:886a12dc4726f5b991b46386292afa8d943b6703a5496c8a1e07cfde778d9044",
# The `SYS_ADMIN` capability is added so that browsers can be launched with sandbox mode enabled. Related
# # information: https://developers.google.com/web/tools/puppeteer/troubleshooting#running_puppeteer_in_docker
"dockerAddCapabilities": "SYS_ADMIN",
# By default in Google Cloud Remote build execution, network access is disabled. We explicitly set the
# property in the platform again in case the default ever changes. Network access is not desirable in
# Bazel builds as it is potential source of flaky tests and therefore also breaks hermeticity.
"dockerNetwork": "off",
},
)
filegroup(
name = "files",
srcs = [
"BUILD.bazel",
"//dev-infra/bazel/remote-execution/cpp:files",
],
)
@@ -1,57 +0,0 @@
load("@bazel_tools//tools/cpp:cc_toolchain_config.bzl", "cc_toolchain_config")
package(default_visibility = ["//visibility:public"])
filegroup(
name = "files",
srcs = ["BUILD.bazel"],
)
cc_toolchain_suite(
name = "cc_toolchain_suite",
toolchains = {
"k8": ":cc_compiler_k8",
},
)
toolchain(
name = "cc_toolchain",
exec_compatible_with = [
"@bazel_tools//platforms:linux",
"@bazel_tools//platforms:x86_64",
"@bazel_tools//tools/cpp:clang",
],
target_compatible_with = [
"@bazel_tools//platforms:linux",
"@bazel_tools//platforms:x86_64",
],
toolchain = ":cc_compiler_k8",
toolchain_type = "@bazel_tools//tools/cpp:toolchain_type",
)
# Basic CC toolchain for k8 remote containers. Based on the default k8
# toolchain provided in Bazel (but unfortunately internal).
# https://github.com/bazelbuild/bazel/blob/c951753097b45cfb9be512c02199aa891b9646b8/tools/cpp/BUILD.tools#L298-L311
cc_toolchain(
name = "cc_compiler_k8",
all_files = ":empty",
ar_files = ":empty",
as_files = ":empty",
compiler_files = ":empty",
dwp_files = ":empty",
linker_files = ":empty",
objcopy_files = ":empty",
strip_files = ":empty",
supports_param_files = 1,
toolchain_config = ":k8_toolchain_config",
toolchain_identifier = "cc-k8-compiler",
)
cc_toolchain_config(
name = "k8_toolchain_config",
compiler = "compiler",
cpu = "local",
)
# Empty filegroup used for defining the CC toolchain.
filegroup(name = "empty")
-11
View File
@@ -1,11 +0,0 @@
package(default_visibility = ["//visibility:public"])
# Make source files available for distribution via pkg_npm
filegroup(
name = "files",
srcs = glob(["*"]) + [
"//dev-infra/benchmark/brotli-cli:files",
"//dev-infra/benchmark/component_benchmark:files",
"//dev-infra/benchmark/ng_rollup_bundle:files",
],
)
@@ -1,19 +0,0 @@
package(default_visibility = ["//visibility:public"])
load("@build_bazel_rules_nodejs//:index.bzl", "nodejs_binary")
nodejs_binary(
name = "brotli-cli",
data = [
"cli.js",
"@npm//brotli",
],
entry_point = ":cli.js",
visibility = ["//visibility:public"],
)
# Make source files available for distribution via pkg_npm
filegroup(
name = "files",
srcs = glob(["*"]),
)
-21
View File
@@ -1,21 +0,0 @@
/**
* @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
*/
const fs = require('fs');
const compress = require('brotli/compress');
function main(args) {
const output = args[0].substring('--output='.length);
const input = args[1];
const buffer = fs.readFileSync(input);
fs.writeFileSync(output, compress(buffer, {mode: 0, quality: 11}));
}
if (require.main === module) {
main(process.argv.slice(2));
}
@@ -1,12 +0,0 @@
package(default_visibility = ["//visibility:public"])
exports_files([
"protractor-perf.conf.js",
"start-server.js",
])
# Make source files available for distribution via pkg_npm
filegroup(
name = "files",
srcs = glob(["*"]) + ["//dev-infra/benchmark/component_benchmark/defaults:files"],
)
@@ -1,27 +0,0 @@
load("@npm//@bazel/protractor:index.bzl", "protractor_web_test_suite")
"""
Macro that can be used to define a benchmark test. This differentiates from
a normal Protractor test suite because we specify a custom "perf" configuration
that sets up "@angular/benchpress". Benchmark test targets will not run on CI
unless explicitly requested.
"""
def benchmark_test(name, server, tags = [], **kwargs):
protractor_web_test_suite(
name = name,
browsers = ["//dev-infra/bazel/browsers/chromium:chromium"],
configuration = "//dev-infra/benchmark/component_benchmark:protractor-perf.conf.js",
on_prepare = "//dev-infra/benchmark/component_benchmark:start-server.js",
server = server,
# Benchmark targets should not run on CI by default.
tags = tags + [
"manual",
"no-remote-exec",
],
test_suite_tags = [
"manual",
"no-remote-exec",
],
**kwargs
)
@@ -1,149 +0,0 @@
load("//dev-infra/benchmark/ng_rollup_bundle:ng_rollup_bundle.bzl", "ng_rollup_bundle")
load("//tools:defaults.bzl", "ng_module")
load("@npm//@bazel/typescript:index.bzl", "ts_library")
load("@npm//@bazel/concatjs:index.bzl", "concatjs_devserver")
load(":benchmark_test.bzl", "benchmark_test")
def copy_default_file(origin, destination):
"""
Copies a file from ./defaults to the destination.
Args:
origin: The name of a file in ./defaults to be copied.
destination: Where the original file will be clopied to.
"""
native.genrule(
name = "copy_default_" + origin + "_file_genrule",
srcs = ["//dev-infra/benchmark/component_benchmark/defaults:" + origin],
outs = [destination],
cmd = "cat $(SRCS) >> $@",
)
def component_benchmark(
name,
prefix,
driver,
driver_deps,
ng_srcs,
ng_deps,
ng_assets = [],
assets = None,
styles = None,
entry_point = None,
entry_point_deps = [
"//packages/core",
"//packages/platform-browser",
]):
"""
Runs a benchmark test against the given angular app using the given driver.
This rule was created with the intention of reducing the amount of
duplicate/boilderplate code, while also allowing you to be as verbose with
your app as you'd like. The goal being that if you just want to test a
simple component, the only thing you'd need to provide are the component
(via ng_srcs) and driver.
** USAGE NOTES **
(assets/styles): The default index.html imports a stylesheet named
"styles.css". This allows the use of the default index.html with a custom
stylesheet through the styles arg by providing either a styles.css in the
prefix directory or by providing a css binary named styles.css.
(assets): The default index.html expects that the root selector for
the benchmark app is "app-root".
(entry_point): The default entry_point expects a file named "app.module" to export
the root NgModule for the benchmark application. It also expects that the
root NgModule is named "AppModule".
TIP: The server is named `name + "_server"` so that you can view/debug the
app.
Args:
name: The name of the benchmark_test to be run
prefix: The relative path to the root directory of the benchmark app
driver: The ts driver for running the benchmark
driver_deps: Driver's dependencies
ng_srcs: All of the ts srcs for the angular app
ng_deps: Dependencies for the angular app
ng_assets: The static assets for the angular app
assets: Static files
styles: Stylesheets
entry_point: Main entry point for the angular app
entry_point_deps: Entry point's dependencies
"""
app_lib = name + "_app_lib"
app_main = name + "_app_main"
benchmark_driver = name + "_driver"
server = name + "_server"
# If the user doesn't provide assets, entry_point, or styles, we use a
# default version.
# Note that we copy the default files to the same directory as what is used
# by the app for three reasons:
# 1. To avoid having the entry point be defined in a different package from
# where this macro is called.
# 2. So that we can use relative paths for imports in entry point.
# 3. To make using default static files as seamless as possible.
if not entry_point:
entry_point = prefix + "default_index.ts"
ng_srcs.append(entry_point)
copy_default_file("index.ts", entry_point)
if not assets:
html = prefix + "index.html"
assets = [html]
copy_default_file("index.html", html)
if not styles:
css = prefix + "styles.css"
styles = [css]
copy_default_file("styles.css", css)
# Bootstraps the application and creates
# additional files to be imported by the entry_point file.
ng_module(
name = app_lib,
srcs = ng_srcs,
assets = ng_assets,
# Creates ngFactory and ngSummary to be imported by the app's entry point.
generate_ve_shims = True,
deps = ng_deps,
tsconfig = "//dev-infra/benchmark/component_benchmark:tsconfig-e2e.json",
)
# Bundle the application (needed by concatjs_devserver).
ng_rollup_bundle(
name = app_main,
entry_point = entry_point,
deps = [":" + app_lib] + entry_point_deps,
)
# The ts_library for the driver that runs tests against the benchmark app.
ts_library(
name = benchmark_driver,
tsconfig = "//dev-infra/benchmark/component_benchmark:tsconfig-e2e.json",
testonly = True,
srcs = [driver],
deps = driver_deps,
)
# The server for our application.
concatjs_devserver(
name = server,
bootstrap = ["//packages/zone.js/bundles:zone.umd.js"],
port = 4200,
static_files = assets + styles,
deps = [":" + app_main + ".min_debug.js"],
additional_root_paths = ["//dev-infra/benchmark/component_benchmark/defaults"],
serving_path = "/app_bundle.js",
)
# Runs a protractor test that's set up to use @angular/benchpress.
benchmark_test(
name = name,
server = ":" + server,
deps = [":" + benchmark_driver],
)
@@ -1,13 +0,0 @@
package(default_visibility = ["//visibility:public"])
# Make source files available for distribution via pkg_npm
filegroup(
name = "files",
srcs = glob(["*"]),
)
exports_files([
"index.html",
"index.ts",
"styles.css",
])
@@ -1,14 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Benchmark Test</title>
<!-- Prevent favicon.ico requests -->
<link rel="icon" href="data:,">
<link rel="stylesheet" href="styles.css">
</head>
<body>
<app-root id="root">Loading...</app-root>
<script src="/app_bundle.js"></script>
</body>
</html>
@@ -1,19 +0,0 @@
/**
* @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
*/
// @ts-ignore Cannot find module
import {enableProdMode} from '@angular/core';
// @ts-ignore Cannot find module
import {platformBrowser} from '@angular/platform-browser';
// @ts-ignore Cannot find module
import {AppModuleNgFactory} from './app.module.ngfactory';
enableProdMode();
platformBrowser().bootstrapModuleFactory(AppModuleNgFactory);
@@ -1,7 +0,0 @@
/*
* This file exists so that if the default index.html is used a 404 will not
* throw.
*
* We leave an import for "styles.css" in the default index.html for the case
* where someone wants to use index.html and provide their own styles.
*/
@@ -1,43 +0,0 @@
/**
* @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
*/
const CHROME_OPTIONS = {
'args': ['--js-flags=--expose-gc', '--no-sandbox', '--headless', '--disable-dev-shm-usage'],
'perfLoggingPrefs': {
'traceCategories':
'v8,blink.console,devtools.timeline,disabled-by-default-devtools.timeline,blink.user_timing'
}
};
exports.config = {
onPrepare: function() {
beforeEach(function() {
browser.ignoreSynchronization = false;
});
},
restartBrowserBetweenTests: true,
allScriptsTimeout: 11000,
capabilities: {
'browserName': 'chrome',
chromeOptions: CHROME_OPTIONS,
loggingPrefs: {
performance: 'ALL',
browser: 'ALL',
}
},
directConnect: true,
framework: 'jasmine2',
jasmineNodeOpts: {
showColors: true,
defaultTimeoutInterval: 90000,
print: function(msg) {
console.info(msg);
},
},
useAllAngular2AppRoots: true
};
@@ -1,18 +0,0 @@
/**
* @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
*/
const protractorUtils = require('@bazel/protractor/protractor-utils');
const protractor = require('protractor');
module.exports = async function(config) {
const {port} = await protractorUtils.runServer(config.workspace, config.server, '-port', []);
const processedConfig = await protractor.browser.getProcessedConfig();
const serverUrl = `http://localhost:${port}`;
return processedConfig.baseUrl = protractor.browser.baseUrl = serverUrl;
};
@@ -1,6 +0,0 @@
{
"compilerOptions": {
"lib": ["es2015", "dom"],
"types": ["node", "jasmine"]
}
}
@@ -1,17 +0,0 @@
load("//dev-infra:defaults.bzl", "ts_library")
package(default_visibility = ["//visibility:public"])
ts_library(
name = "driver-utilities",
srcs = glob(["*.ts"]),
tsconfig = "//dev-infra/benchmark/component_benchmark:tsconfig-e2e.json",
deps = [
"//packages/benchpress",
"@npm//@types/node",
"@npm//@types/selenium-webdriver",
"@npm//node-uuid",
"@npm//protractor",
"@npm//selenium-webdriver",
],
)
@@ -1,50 +0,0 @@
/**
* @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
*/
/* tslint:disable:no-console */
import {browser} from 'protractor';
import * as webdriver from 'selenium-webdriver';
declare var expect: any;
export function openBrowser(config: {
url?: string,
params?: {name: string, value: any}[],
ignoreBrowserSynchronization?: boolean
}) {
if (config.ignoreBrowserSynchronization) {
browser.ignoreSynchronization = true;
}
const urlParams: string[] = [];
if (config.params) {
config.params.forEach((param) => urlParams.push(param.name + '=' + param.value));
}
const url = encodeURI(config.url + '?' + urlParams.join('&'));
browser.get(url);
if (config.ignoreBrowserSynchronization) {
browser.sleep(2000);
}
}
/**
* @experimental This API will be moved to Protractor.
*/
export function verifyNoBrowserErrors() {
// TODO(tbosch): Bug in ChromeDriver: Need to execute at least one command
// so that the browser logs can be read out!
browser.executeScript('1+1');
browser.manage().logs().get('browser').then(function(browserLog: any) {
const filteredLog = browserLog.filter(function(logEntry: any) {
if (logEntry.level.value >= webdriver.logging.Level.INFO.value) {
console.log('>> ' + logEntry.message);
}
return logEntry.level.value > webdriver.logging.Level.WARNING.value;
});
expect(filteredLog).toEqual([]);
});
}
@@ -1,9 +0,0 @@
/**
* @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
*/
export {openBrowser, verifyNoBrowserErrors} from './e2e_util';
export {runBenchmark} from './perf_util';
@@ -1,85 +0,0 @@
/**
* @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 {mkdirSync} from 'fs';
export {verifyNoBrowserErrors} from './e2e_util';
const nodeUuid = require('node-uuid');
import {SeleniumWebDriverAdapter, Options, JsonFileReporter, Validator, RegressionSlopeValidator, ConsoleReporter, SizeValidator, MultiReporter, MultiMetric, Runner, StaticProvider} from '@angular/benchpress';
import {openBrowser} from './e2e_util';
// Note: Keep the `modules/benchmarks/README.md` file in sync with the supported options.
const globalOptions = {
sampleSize: process.env.PERF_SAMPLE_SIZE || 20,
forceGc: process.env.PERF_FORCE_GC === 'true',
dryRun: process.env.PERF_DRYRUN === 'true',
};
const runner = createBenchpressRunner();
export async function runBenchmark({
id,
url = '',
params = [],
ignoreBrowserSynchronization = true,
microMetrics,
work,
prepare,
setup,
}: {
id: string,
url?: string,
params?: {name: string, value: any}[],
ignoreBrowserSynchronization?: boolean,
microMetrics?: {[key: string]: string},
work?: (() => void)|(() => Promise<unknown>),
prepare?: (() => void)|(() => Promise<unknown>),
setup?: (() => void)|(() => Promise<unknown>),
}): Promise<any> {
openBrowser({url, params, ignoreBrowserSynchronization});
if (setup) {
await setup();
}
return runner.sample({
id,
execute: work,
prepare,
microMetrics,
providers: [{provide: Options.SAMPLE_DESCRIPTION, useValue: {}}]
});
}
function createBenchpressRunner(): Runner {
let runId = nodeUuid.v1();
if (process.env.GIT_SHA) {
runId = process.env.GIT_SHA + ' ' + runId;
}
const resultsFolder = './dist/benchmark_results';
mkdirSync(resultsFolder, {
recursive: true,
});
const providers: StaticProvider[] = [
SeleniumWebDriverAdapter.PROTRACTOR_PROVIDERS,
{provide: Options.FORCE_GC, useValue: globalOptions.forceGc},
{provide: Options.DEFAULT_DESCRIPTION, useValue: {'runId': runId}}, JsonFileReporter.PROVIDERS,
{provide: JsonFileReporter.PATH, useValue: resultsFolder}
];
if (!globalOptions.dryRun) {
providers.push({provide: Validator, useExisting: RegressionSlopeValidator});
providers.push(
{provide: RegressionSlopeValidator.SAMPLE_SIZE, useValue: globalOptions.sampleSize});
providers.push(MultiReporter.provideWith([ConsoleReporter, JsonFileReporter]));
} else {
providers.push({provide: Validator, useExisting: SizeValidator});
providers.push({provide: SizeValidator.SAMPLE_SIZE, useValue: 1});
providers.push(MultiReporter.provideWith([]));
providers.push(MultiMetric.provideWith([]));
}
return new Runner(providers);
}
@@ -1,26 +0,0 @@
package(default_visibility = ["//visibility:public"])
load("@build_bazel_rules_nodejs//:index.bzl", "nodejs_binary")
exports_files([
"rollup.config-tmpl.js",
"terser_config.json",
])
nodejs_binary(
name = "rollup_with_build_optimizer",
data = [
"@npm//@angular-devkit/build-optimizer",
"@npm//rollup",
"@npm//rollup-plugin-commonjs",
"@npm//rollup-plugin-node-resolve",
"@npm//rollup-plugin-sourcemaps",
],
entry_point = "@npm//:node_modules/rollup/dist/bin/rollup",
)
# Make source files available for distribution via pkg_npm
filegroup(
name = "files",
srcs = glob(["*"]),
)
@@ -1,104 +0,0 @@
# 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
load("@build_bazel_rules_nodejs//:index.bzl", "npm_package_bin")
load("@npm//@bazel/terser:index.bzl", "terser_minified")
load("@npm//@bazel/rollup:index.bzl", "rollup_bundle")
load("//dev-infra/bazel:expand_template.bzl", "expand_template")
def ng_rollup_bundle(
name,
entry_point,
deps = [],
license_banner = None,
build_optimizer = True,
visibility = None,
format = "iife",
globals = {},
**kwargs):
"""Rollup with Build Optimizer on target prodmode output (ESM2015).
This provides an extension of the [rollup_bundle] rule that works better for Angular apps.
Runs [rollup], [terser_minified] and [brotli] to produce a number of output bundles.
es2015 : "%{name}.js"
es2015 minified : "%{name}.min.js"
es2015 minified (compressed) : "%{name}.min.js.br",
es2015 minified (debug) : "%{name}.min_debug.js"
It registers `@angular-devkit/build-optimizer` as a rollup plugin by default. This helps
with further optimization. See https://github.com/angular/angular-cli/tree/master/packages/angular_devkit/build_optimizer.
[rollup_bundle]: https://github.com/bazelbuild/rules_nodejs/blob/1.x/packages/rollup/src/rollup_bundle.bzl
[rollup]: https://rollupjs.org/guide/en/
[terser_minified]: https://bazelbuild.github.io/rules_nodejs/Terser.html
[brotli]: https://brotli.org/
"""
config_data = [license_banner] if license_banner else []
expand_template(
name = "%s_rollup_config" % name,
template = "//dev-infra/benchmark/ng_rollup_bundle:rollup.config-tmpl.js",
output_name = "%s_rollup_config.js" % name,
configuration_env_vars = ["angular_ivy_enabled"],
data = config_data,
substitutions = {
"TMPL_build_optimizer": "true" if build_optimizer else "false",
"TMPL_banner_file": "\"$(execpath %s)\"" % license_banner if license_banner else "undefined",
"TMPL_external": ", ".join(["'%s'" % e for e in globals.keys()]),
"TMPL_globals": ", ".join(["'%s': '%s'" % (g, g) for g in globals]),
},
visibility = visibility,
)
rollup_bundle(
name = name,
config_file = "%s_rollup_config" % name,
entry_points = {
(entry_point): name,
},
visibility = visibility,
deps = config_data + deps + [
"@npm//rollup-plugin-node-resolve",
"@npm//rollup-plugin-sourcemaps",
"@npm//rollup-plugin-commonjs",
"@npm//@angular-devkit/build-optimizer",
],
silent = True,
format = format,
sourcemap = "true",
# TODO(devversion): consider removing when View Engine is removed. View Engine
# uses Bazel manifest path imports in generated factory files.
# e.g. `import "<workspace_root>/<..>/some_file";`
link_workspace_root = True,
**kwargs
)
common_terser_options = {
"visibility": visibility,
"config_file": "//dev-infra/benchmark/ng_rollup_bundle:terser_config.json",
# TODO: Enable source maps for better debugging when `@bazel/terser` pre-declares
# JS and map outputs. Tracked with: DEV-120
"sourcemap": False,
}
terser_minified(name = name + ".min", src = name + ".js", **common_terser_options)
native.filegroup(name = name + ".min.js", srcs = [name + ".min"], visibility = visibility)
terser_minified(name = name + ".min_debug", src = name + ".js", debug = True, **common_terser_options)
native.filegroup(name = name + ".min_debug.js", srcs = [name + ".min_debug"], visibility = visibility)
npm_package_bin(
name = "_%s_brotli" % name,
tool = "//dev-infra/benchmark/brotli-cli",
data = [name + ".min.js"],
outs = [name + ".min.js.br"],
args = [
"--output=$(execpath %s.min.js.br)" % name,
"$(execpath %s.min.js)" % name,
],
visibility = visibility,
)
@@ -1,103 +0,0 @@
/**
* @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
*/
// Rollup configuration
// GENERATED BY Bazel
const buildOptimizer =
require('@angular-devkit/build-optimizer/src/build-optimizer/rollup-plugin.js');
const nodeResolve = require('rollup-plugin-node-resolve');
const sourcemaps = require('rollup-plugin-sourcemaps');
const commonjs = require('rollup-plugin-commonjs');
const path = require('path');
const fs = require('fs');
function log_verbose(...m) {
// This is a template file so we use __filename to output the actual filename
if (!!process.env['VERBOSE_LOGS']) console.error(`[${path.basename(__filename)}]`, ...m);
}
// Substitutions from the `ng_rollup_bundle` macro. We want to conditionally toggle
// build optimizer, support optional banner files, and generally respect the current
// compilation mode (i.e. Ivy or View Engine) as that affects module resolution.
const useBuildOptimizer = TMPL_build_optimizer;
const bannerFile = TMPL_banner_file;
const ivyEnabled = 'TMPL_angular_ivy_enabled' === 'True';
// `bazel_version_file` is a substitution that is applied by `@bazel/rollup`.
const stampDataFile = bazel_version_file;
log_verbose(`running with
cwd: ${process.cwd()}
useBuildOptimizer: ${useBuildOptimizer}
bannerFile: ${bannerFile}
stampDataFile: ${stampDataFile}
ivyEnabled: ${ivyEnabled}
`);
const plugins = [
nodeResolve({
// If Ivy is enabled, we need to make sure that the module resolution prioritizes ngcc
// processed entry-point fields. Ngcc adds special fields to `package.json` files of
// modules that have been processed. Prioritizing these fields matches the Angular CLIs
// behavior for supporting Ivy. We need to support ngcc because `ng_rollup_bundle` rule is
// shared with other repositories that consume Angular from NPM (w/ ngcc).
// https://github.com/angular/angular-cli/blob/1a1ceb609b9a87c4021cce3a6f0fc6d167cd09d2/packages/ngtools/webpack/src/angular_compiler_plugin.ts#L918-L920
mainFields: ivyEnabled ? ['module_ivy_ngcc', 'main_ivy_ngcc', 'module', 'main'] :
['module', 'main'],
preferBuiltins: true,
}),
commonjs({ignoreGlobal: true}),
sourcemaps(),
];
if (useBuildOptimizer) {
plugins.unshift(buildOptimizer.default({
sideEffectFreeModules: [],
}));
}
module.exports = {
plugins,
onwarn: customWarningHandler,
external: [TMPL_external],
output: {
globals: {TMPL_globals},
banner: extractBannerIfConfigured(),
}
};
/** Custom warning handler for Rollup. */
function customWarningHandler(warning, defaultHandler) {
// If rollup is unable to resolve an import, we want to throw an error
// instead of silently treating the import as external dependency.
// https://rollupjs.org/guide/en/#warning-treating-module-as-external-dependency
if (warning.code === 'UNRESOLVED_IMPORT') {
throw Error(`Unresolved import: ${warning.message}`);
}
defaultHandler(warning);
}
/** Extracts the top-level bundle banner if specified. */
function extractBannerIfConfigured() {
if (!bannerFile) {
return undefined;
}
let banner = fs.readFileSync(bannerFile, 'utf8');
if (stampDataFile) {
const versionTag = fs.readFileSync(stampDataFile, 'utf8')
.split('\n')
.find(s => s.startsWith('BUILD_SCM_VERSION'));
// Don't assume BUILD_SCM_VERSION exists
if (versionTag) {
const version = versionTag.split(' ')[1].trim();
banner = banner.replace(/0.0.0-PLACEHOLDER/, version);
}
}
return banner;
}
@@ -1,18 +0,0 @@
{
"output": {
"ecma": "es2015",
"comments": false,
"beautify": "bazel_debug"
},
"compress": {
"global_defs": {
"ngDevMode": false,
"ngI18nClosureMode": false,
"ngJitMode": false
},
"passes": 3,
"pure_getters": true
},
"toplevel": true,
"mangle": "bazel_no_debug"
}
-609
View File
@@ -1,609 +0,0 @@
'use strict';
var tslib = require('tslib');
var fs = require('fs');
var path = require('path');
require('chalk');
require('inquirer');
var child_process = require('child_process');
var semver = require('semver');
var graphql = require('@octokit/graphql');
var rest = require('@octokit/rest');
var typedGraphqlify = require('typed-graphqlify');
var url = require('url');
/**
* @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
*/
/** Whether the current environment is in dry run mode. */
function isDryRun() {
return process.env['DRY_RUN'] !== undefined;
}
/** Error to be thrown when a function or method is called in dryRun mode and shouldn't be. */
var DryRunError = /** @class */ (function (_super) {
tslib.__extends(DryRunError, _super);
function DryRunError() {
var _this = _super.call(this, 'Cannot call this function in dryRun mode.') || this;
// Set the prototype explicitly because in ES5, the prototype is accidentally lost due to
// a limitation in down-leveling.
// https://github.com/Microsoft/TypeScript/wiki/FAQ#why-doesnt-extending-built-ins-like-error-array-and-map-work.
Object.setPrototypeOf(_this, DryRunError.prototype);
return _this;
}
return DryRunError;
}(Error));
/**
* @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
*/
/** Error for failed Github API requests. */
var GithubApiRequestError = /** @class */ (function (_super) {
tslib.__extends(GithubApiRequestError, _super);
function GithubApiRequestError(status, message) {
var _this = _super.call(this, message) || this;
_this.status = status;
return _this;
}
return GithubApiRequestError;
}(Error));
/** A Github client for interacting with the Github APIs. */
var GithubClient = /** @class */ (function () {
function GithubClient(_octokitOptions) {
this._octokitOptions = _octokitOptions;
/** The octokit instance actually performing API requests. */
this._octokit = new rest.Octokit(this._octokitOptions);
this.pulls = this._octokit.pulls;
this.repos = this._octokit.repos;
this.issues = this._octokit.issues;
this.git = this._octokit.git;
this.rateLimit = this._octokit.rateLimit;
this.teams = this._octokit.teams;
// Note: These are properties from `Octokit` that are brought in by optional plugins.
// TypeScript requires us to provide an explicit type for these.
this.rest = this._octokit.rest;
this.paginate = this._octokit.paginate;
}
return GithubClient;
}());
/**
* Extension of the `GithubClient` that provides utilities which are specific
* to authenticated instances.
*/
var AuthenticatedGithubClient = /** @class */ (function (_super) {
tslib.__extends(AuthenticatedGithubClient, _super);
function AuthenticatedGithubClient(_token) {
var _this =
// Set the token for the octokit instance.
_super.call(this, { auth: _token }) || this;
_this._token = _token;
/** The graphql instance with authentication set during construction. */
_this._graphql = graphql.graphql.defaults({ headers: { authorization: "token " + _this._token } });
return _this;
}
/** Perform a query using Github's Graphql API. */
AuthenticatedGithubClient.prototype.graphql = function (queryObject, params) {
if (params === void 0) { params = {}; }
return tslib.__awaiter(this, void 0, void 0, function () {
return tslib.__generator(this, function (_a) {
switch (_a.label) {
case 0: return [4 /*yield*/, this._graphql(typedGraphqlify.query(queryObject).toString(), params)];
case 1: return [2 /*return*/, (_a.sent())];
}
});
});
};
return AuthenticatedGithubClient;
}(GithubClient));
/**
* @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
*/
/** Adds the provided token to the given Github HTTPs remote url. */
function addTokenToGitHttpsUrl(githubHttpsUrl, token) {
var url$1 = new url.URL(githubHttpsUrl);
url$1.username = token;
return url$1.href;
}
/** Gets the repository Git URL for the given github config. */
function getRepositoryGitUrl(config, githubToken) {
if (config.useSsh) {
return "git@github.com:" + config.owner + "/" + config.name + ".git";
}
var baseHttpUrl = "https://github.com/" + config.owner + "/" + config.name + ".git";
if (githubToken !== undefined) {
return addTokenToGitHttpsUrl(baseHttpUrl, githubToken);
}
return baseHttpUrl;
}
/**
* @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
*/
/** Error for failed Git commands. */
var GitCommandError = /** @class */ (function (_super) {
tslib.__extends(GitCommandError, _super);
function GitCommandError(client, args) {
var _this =
// Errors are not guaranteed to be caught. To ensure that we don't
// accidentally leak the Github token that might be used in a command,
// we sanitize the command that will be part of the error message.
_super.call(this, "Command failed: git " + client.sanitizeConsoleOutput(args.join(' '))) || this;
_this.args = args;
// Set the prototype explicitly because in ES5, the prototype is accidentally lost due to
// a limitation in down-leveling.
// https://github.com/Microsoft/TypeScript/wiki/FAQ#why-doesnt-extending-built-ins-like-error-array-and-map-work.
Object.setPrototypeOf(_this, GitCommandError.prototype);
return _this;
}
return GitCommandError;
}(Error));
/** Class that can be used to perform Git interactions with a given remote. **/
var GitClient = /** @class */ (function () {
function GitClient(
/** The full path to the root of the repository base. */
baseDir,
/** The configuration, containing the github specific configuration. */
config) {
if (baseDir === void 0) { baseDir = determineRepoBaseDirFromCwd(); }
if (config === void 0) { config = getConfig(baseDir); }
this.baseDir = baseDir;
this.config = config;
/** Short-hand for accessing the default remote configuration. */
this.remoteConfig = this.config.github;
/** Octokit request parameters object for targeting the configured remote. */
this.remoteParams = { owner: this.remoteConfig.owner, repo: this.remoteConfig.name };
/** Instance of the Github client. */
this.github = new GithubClient();
}
/** Executes the given git command. Throws if the command fails. */
GitClient.prototype.run = function (args, options) {
var result = this.runGraceful(args, options);
if (result.status !== 0) {
throw new GitCommandError(this, args);
}
// Omit `status` from the type so that it's obvious that the status is never
// non-zero as explained in the method description.
return result;
};
/**
* Spawns a given Git command process. Does not throw if the command fails. Additionally,
* if there is any stderr output, the output will be printed. This makes it easier to
* info failed commands.
*/
GitClient.prototype.runGraceful = function (args, options) {
if (options === void 0) { options = {}; }
/** The git command to be run. */
var gitCommand = args[0];
if (isDryRun() && gitCommand === 'push') {
debug("\"git push\" is not able to be run in dryRun mode.");
throw new DryRunError();
}
// To improve the debugging experience in case something fails, we print all executed Git
// commands at the DEBUG level to better understand the git actions occurring. Verbose logging,
// always logging at the INFO level, can be enabled either by setting the verboseLogging
// property on the GitClient class or the options object provided to the method.
var printFn = (GitClient.verboseLogging || options.verboseLogging) ? info : debug;
// Note that we sanitize the command before printing it to the console. We do not want to
// print an access token if it is contained in the command. It's common to share errors with
// others if the tool failed, and we do not want to leak tokens.
printFn('Executing: git', this.sanitizeConsoleOutput(args.join(' ')));
var result = child_process.spawnSync('git', args, tslib.__assign(tslib.__assign({ cwd: this.baseDir, stdio: 'pipe' }, options), {
// Encoding is always `utf8` and not overridable. This ensures that this method
// always returns `string` as output instead of buffers.
encoding: 'utf8' }));
if (result.stderr !== null) {
// Git sometimes prints the command if it failed. This means that it could
// potentially leak the Github token used for accessing the remote. To avoid
// printing a token, we sanitize the string before printing the stderr output.
process.stderr.write(this.sanitizeConsoleOutput(result.stderr));
}
return result;
};
/** Git URL that resolves to the configured repository. */
GitClient.prototype.getRepoGitUrl = function () {
return getRepositoryGitUrl(this.remoteConfig);
};
/** Whether the given branch contains the specified SHA. */
GitClient.prototype.hasCommit = function (branchName, sha) {
return this.run(['branch', branchName, '--contains', sha]).stdout !== '';
};
/** Gets the currently checked out branch or revision. */
GitClient.prototype.getCurrentBranchOrRevision = function () {
var branchName = this.run(['rev-parse', '--abbrev-ref', 'HEAD']).stdout.trim();
// If no branch name could be resolved. i.e. `HEAD` has been returned, then Git
// is currently in a detached state. In those cases, we just want to return the
// currently checked out revision/SHA.
if (branchName === 'HEAD') {
return this.run(['rev-parse', 'HEAD']).stdout.trim();
}
return branchName;
};
/** Gets whether the current Git repository has uncommitted changes. */
GitClient.prototype.hasUncommittedChanges = function () {
return this.runGraceful(['diff-index', '--quiet', 'HEAD']).status !== 0;
};
/**
* Checks out a requested branch or revision, optionally cleaning the state of the repository
* before attempting the checking. Returns a boolean indicating whether the branch or revision
* was cleanly checked out.
*/
GitClient.prototype.checkout = function (branchOrRevision, cleanState) {
if (cleanState) {
// Abort any outstanding ams.
this.runGraceful(['am', '--abort'], { stdio: 'ignore' });
// Abort any outstanding cherry-picks.
this.runGraceful(['cherry-pick', '--abort'], { stdio: 'ignore' });
// Abort any outstanding rebases.
this.runGraceful(['rebase', '--abort'], { stdio: 'ignore' });
// Clear any changes in the current repo.
this.runGraceful(['reset', '--hard'], { stdio: 'ignore' });
}
return this.runGraceful(['checkout', branchOrRevision], { stdio: 'ignore' }).status === 0;
};
/** Gets the latest git tag on the current branch that matches SemVer. */
GitClient.prototype.getLatestSemverTag = function () {
var semVerOptions = { loose: true };
var tags = this.runGraceful(['tag', '--sort=-committerdate', '--merged']).stdout.split('\n');
var latestTag = tags.find(function (tag) { return semver.parse(tag, semVerOptions); });
if (latestTag === undefined) {
throw new Error("Unable to find a SemVer matching tag on \"" + this.getCurrentBranchOrRevision() + "\"");
}
return new semver.SemVer(latestTag, semVerOptions);
};
/** Retrieves the git tag matching the provided SemVer, if it exists. */
GitClient.prototype.getMatchingTagForSemver = function (semver$1) {
var semVerOptions = { loose: true };
var tags = this.runGraceful(['tag', '--sort=-committerdate', '--merged']).stdout.split('\n');
var matchingTag = tags.find(function (tag) { var _a; return ((_a = semver.parse(tag, semVerOptions)) === null || _a === void 0 ? void 0 : _a.compare(semver$1)) === 0; });
if (matchingTag === undefined) {
throw new Error("Unable to find a tag for the version: \"" + semver$1.format() + "\"");
}
return matchingTag;
};
/** Retrieve a list of all files in the repository changed since the provided shaOrRef. */
GitClient.prototype.allChangesFilesSince = function (shaOrRef) {
if (shaOrRef === void 0) { shaOrRef = 'HEAD'; }
return Array.from(new Set(tslib.__spreadArray(tslib.__spreadArray([], tslib.__read(gitOutputAsArray(this.runGraceful(['diff', '--name-only', '--diff-filter=d', shaOrRef])))), tslib.__read(gitOutputAsArray(this.runGraceful(['ls-files', '--others', '--exclude-standard']))))));
};
/** Retrieve a list of all files currently staged in the repostitory. */
GitClient.prototype.allStagedFiles = function () {
return gitOutputAsArray(this.runGraceful(['diff', '--name-only', '--diff-filter=ACM', '--staged']));
};
/** Retrieve a list of all files tracked in the repository. */
GitClient.prototype.allFiles = function () {
return gitOutputAsArray(this.runGraceful(['ls-files']));
};
/**
* Sanitizes the given console message. This method can be overridden by
* derived classes. e.g. to sanitize access tokens from Git commands.
*/
GitClient.prototype.sanitizeConsoleOutput = function (value) {
return value;
};
/** Set the verbose logging state of all git client instances. */
GitClient.setVerboseLoggingState = function (verbose) {
GitClient.verboseLogging = verbose;
};
/**
* Static method to get the singleton instance of the `GitClient`, creating it
* if it has not yet been created.
*/
GitClient.get = function () {
if (!this._unauthenticatedInstance) {
GitClient._unauthenticatedInstance = new GitClient();
}
return GitClient._unauthenticatedInstance;
};
/** Whether verbose logging of Git actions should be used. */
GitClient.verboseLogging = false;
return GitClient;
}());
/**
* Takes the output from `run` and `runGraceful` and returns an array of strings for each
* new line. Git commands typically return multiple output values for a command a set of
* strings separated by new lines.
*
* Note: This is specifically created as a locally available function for usage as convenience
* utility within `GitClient`'s methods to create outputs as array.
*/
function gitOutputAsArray(gitCommandResult) {
return gitCommandResult.stdout.split('\n').map(function (x) { return x.trim(); }).filter(function (x) { return !!x; });
}
/** Determines the repository base directory from the current working directory. */
function determineRepoBaseDirFromCwd() {
// TODO(devversion): Replace with common spawn sync utility once available.
var _a = child_process.spawnSync('git', ['rev-parse --show-toplevel'], { shell: true, stdio: 'pipe', encoding: 'utf8' }), stdout = _a.stdout, stderr = _a.stderr, status = _a.status;
if (status !== 0) {
throw Error("Unable to find the path to the base directory of the repository.\n" +
"Was the command run from inside of the repo?\n\n" +
("" + stderr));
}
return stdout.trim();
}
/**
* @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
*/
/**
* Supported levels for logging functions.
*
* Levels are mapped to numbers to represent a hierarchy of logging levels.
*/
var LOG_LEVELS;
(function (LOG_LEVELS) {
LOG_LEVELS[LOG_LEVELS["SILENT"] = 0] = "SILENT";
LOG_LEVELS[LOG_LEVELS["ERROR"] = 1] = "ERROR";
LOG_LEVELS[LOG_LEVELS["WARN"] = 2] = "WARN";
LOG_LEVELS[LOG_LEVELS["LOG"] = 3] = "LOG";
LOG_LEVELS[LOG_LEVELS["INFO"] = 4] = "INFO";
LOG_LEVELS[LOG_LEVELS["DEBUG"] = 5] = "DEBUG";
})(LOG_LEVELS || (LOG_LEVELS = {}));
/** Default log level for the tool. */
var DEFAULT_LOG_LEVEL = LOG_LEVELS.INFO;
/** Write to the console for at INFO logging level */
var info = buildLogLevelFunction(function () { return console.info; }, LOG_LEVELS.INFO);
/** Write to the console for at ERROR logging level */
var error = buildLogLevelFunction(function () { return console.error; }, LOG_LEVELS.ERROR);
/** Write to the console for at DEBUG logging level */
var debug = buildLogLevelFunction(function () { return console.debug; }, LOG_LEVELS.DEBUG);
/** Write to the console for at LOG logging level */
// tslint:disable-next-line: no-console
var log = buildLogLevelFunction(function () { return console.log; }, LOG_LEVELS.LOG);
/** Write to the console for at WARN logging level */
var warn = buildLogLevelFunction(function () { return console.warn; }, LOG_LEVELS.WARN);
/** Build an instance of a logging function for the provided level. */
function buildLogLevelFunction(loadCommand, level) {
/** Write to stdout for the LOG_LEVEL. */
var loggingFunction = function () {
var text = [];
for (var _i = 0; _i < arguments.length; _i++) {
text[_i] = arguments[_i];
}
runConsoleCommand.apply(void 0, tslib.__spreadArray([loadCommand, level], tslib.__read(text)));
};
/** Start a group at the LOG_LEVEL, optionally starting it as collapsed. */
loggingFunction.group = function (text, collapsed) {
if (collapsed === void 0) { collapsed = false; }
var command = collapsed ? console.groupCollapsed : console.group;
runConsoleCommand(function () { return command; }, level, text);
};
/** End the group at the LOG_LEVEL. */
loggingFunction.groupEnd = function () {
runConsoleCommand(function () { return console.groupEnd; }, level);
};
return loggingFunction;
}
/**
* Run the console command provided, if the environments logging level greater than the
* provided logging level.
*
* The loadCommand takes in a function which is called to retrieve the console.* function
* to allow for jasmine spies to still work in testing. Without this method of retrieval
* the console.* function, the function is saved into the closure of the created logging
* function before jasmine can spy.
*/
function runConsoleCommand(loadCommand, logLevel) {
var text = [];
for (var _i = 2; _i < arguments.length; _i++) {
text[_i - 2] = arguments[_i];
}
if (getLogLevel() >= logLevel) {
loadCommand().apply(void 0, tslib.__spreadArray([], tslib.__read(text)));
}
printToLogFile.apply(void 0, tslib.__spreadArray([logLevel], tslib.__read(text)));
}
/**
* Retrieve the log level from environment variables, if the value found
* based on the LOG_LEVEL environment variable is undefined, return the default
* logging level.
*/
function getLogLevel() {
var logLevelEnvValue = (process.env["LOG_LEVEL"] || '').toUpperCase();
var logLevel = LOG_LEVELS[logLevelEnvValue];
if (logLevel === undefined) {
return DEFAULT_LOG_LEVEL;
}
return logLevel;
}
/**
* The number of columns used in the prepended log level information on each line of the logging
* output file.
*/
var LOG_LEVEL_COLUMNS = 7;
/** Write the provided text to the log file, prepending each line with the log level. */
function printToLogFile(logLevel) {
var text = [];
for (var _i = 1; _i < arguments.length; _i++) {
text[_i - 1] = arguments[_i];
}
var logLevelText = (LOG_LEVELS[logLevel] + ":").padEnd(LOG_LEVEL_COLUMNS);
}
/**
* @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
*/
/** Whether ts-node has been installed and is available to ng-dev. */
function isTsNodeAvailable() {
try {
require.resolve('ts-node');
return true;
}
catch (_a) {
return false;
}
}
/**
* @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
*/
/**
* The filename expected for creating the ng-dev config, without the file
* extension to allow either a typescript or javascript file to be used.
*/
var CONFIG_FILE_PATH = '.ng-dev/config';
/** The configuration for ng-dev. */
var cachedConfig = null;
function getConfig(baseDir) {
// If the global config is not defined, load it from the file system.
if (cachedConfig === null) {
baseDir = baseDir || GitClient.get().baseDir;
// The full path to the configuration file.
var configPath = path.join(baseDir, CONFIG_FILE_PATH);
// Read the configuration and validate it before caching it for the future.
cachedConfig = validateCommonConfig(readConfigFile(configPath));
}
// Return a clone of the cached global config to ensure that a new instance of the config
// is returned each time, preventing unexpected effects of modifications to the config object.
return tslib.__assign({}, cachedConfig);
}
/** Validate the common configuration has been met for the ng-dev command. */
function validateCommonConfig(config) {
var errors = [];
// Validate the github configuration.
if (config.github === undefined) {
errors.push("Github repository not configured. Set the \"github\" option.");
}
else {
if (config.github.name === undefined) {
errors.push("\"github.name\" is not defined");
}
if (config.github.owner === undefined) {
errors.push("\"github.owner\" is not defined");
}
}
assertNoErrors(errors);
return config;
}
/**
* Resolves and reads the specified configuration file, optionally returning an empty object if the
* configuration file cannot be read.
*/
function readConfigFile(configPath, returnEmptyObjectOnError) {
if (returnEmptyObjectOnError === void 0) { returnEmptyObjectOnError = false; }
// If the `.ts` extension has not been set up already, and a TypeScript based
// version of the given configuration seems to exist, set up `ts-node` if available.
if (require.extensions['.ts'] === undefined && fs.existsSync(configPath + ".ts") &&
isTsNodeAvailable()) {
// Ensure the module target is set to `commonjs`. This is necessary because the
// dev-infra tool runs in NodeJS which does not support ES modules by default.
// Additionally, set the `dir` option to the directory that contains the configuration
// file. This allows for custom compiler options (such as `--strict`).
require('ts-node').register({ dir: path.dirname(configPath), transpileOnly: true, compilerOptions: { module: 'commonjs' } });
}
try {
return require(configPath);
}
catch (e) {
if (returnEmptyObjectOnError) {
debug("Could not read configuration file at " + configPath + ", returning empty object instead.");
debug(e);
return {};
}
error("Could not read configuration file at " + configPath + ".");
error(e);
process.exit(1);
}
}
/**
* Asserts the provided array of error messages is empty. If any errors are in the array,
* logs the errors and exit the process as a failure.
*/
function assertNoErrors(errors) {
var e_1, _a;
if (errors.length == 0) {
return;
}
error("Errors discovered while loading configuration file:");
try {
for (var errors_1 = tslib.__values(errors), errors_1_1 = errors_1.next(); !errors_1_1.done; errors_1_1 = errors_1.next()) {
var err = errors_1_1.value;
error(" - " + err);
}
}
catch (e_1_1) { e_1 = { error: e_1_1 }; }
finally {
try {
if (errors_1_1 && !errors_1_1.done && (_a = errors_1.return)) _a.call(errors_1);
}
finally { if (e_1) throw e_1.error; }
}
process.exit(1);
}
/**
* @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
*/
/** Retrieve and validate the config as `ReleaseConfig`. */
function getReleaseConfig(config = getConfig()) {
var _a, _b, _c;
// List of errors encountered validating the config.
const errors = [];
if (config.release === undefined) {
errors.push(`No configuration defined for "release"`);
}
if (((_a = config.release) === null || _a === void 0 ? void 0 : _a.npmPackages) === undefined) {
errors.push(`No "npmPackages" configured for releasing.`);
}
if (((_b = config.release) === null || _b === void 0 ? void 0 : _b.buildPackages) === undefined) {
errors.push(`No "buildPackages" function configured for releasing.`);
}
if (((_c = config.release) === null || _c === void 0 ? void 0 : _c.releaseNotes) === undefined) {
errors.push(`No "releaseNotes" configured for releasing.`);
}
assertNoErrors(errors);
return config.release;
}
/**
* @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
*/
// Start the release package building.
main(process.argv[2] === 'true');
/** Main function for building the release packages. */
function main(stampForRelease) {
return tslib.__awaiter(this, void 0, void 0, function* () {
if (process.send === undefined) {
throw Error('This script needs to be invoked as a NodeJS worker.');
}
const config = getReleaseConfig();
const builtPackages = yield config.buildPackages(stampForRelease);
// Transfer the built packages back to the parent process.
process.send(builtPackages);
});
}
-47
View File
@@ -1,47 +0,0 @@
load("//dev-infra:defaults.bzl", "jasmine_node_test", "ts_library")
ts_library(
name = "caretaker",
srcs = glob(
["**/*.ts"],
exclude = ["**/*.spec.ts"],
),
visibility = ["//dev-infra:__subpackages__"],
deps = [
"//dev-infra/release/versioning",
"//dev-infra/utils",
"@npm//@types/inquirer",
"@npm//@types/node",
"@npm//@types/node-fetch",
"@npm//@types/yargs",
"@npm//inquirer",
"@npm//multimatch",
"@npm//node-fetch",
"@npm//typed-graphqlify",
"@npm//yaml",
"@npm//yargs",
],
)
ts_library(
name = "test_lib",
testonly = True,
srcs = glob(["**/*.spec.ts"]),
deps = [
":caretaker",
"//dev-infra/release/versioning",
"//dev-infra/utils",
"//dev-infra/utils/testing",
"@npm//@types/jasmine",
"@npm//@types/node",
"@npm//@types/semver",
"@npm//semver",
],
)
jasmine_node_test(
name = "test",
deps = [
":test_lib",
],
)
-42
View File
@@ -1,42 +0,0 @@
/**
* @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 {installVirtualGitClientSpies} from '../../utils/testing';
import {BaseModule} from './base';
/** Data mocking as the "retrieved data". */
const exampleData = 'this is example data' as const;
/** A simple usage of the BaseModule to illustrate the workings built into the abstract class. */
class ConcreteBaseModule extends BaseModule<typeof exampleData> {
override async retrieveData() {
return exampleData;
}
override async printToTerminal() {}
}
describe('BaseModule', () => {
let retrieveDataSpy: jasmine.Spy;
beforeEach(() => {
retrieveDataSpy = spyOn(ConcreteBaseModule.prototype, 'retrieveData');
installVirtualGitClientSpies();
});
it('begins retrieving data during construction', () => {
new ConcreteBaseModule({} as any);
expect(retrieveDataSpy).toHaveBeenCalled();
});
it('makes the data available via the data attribute', async () => {
retrieveDataSpy.and.callThrough();
const module = new ConcreteBaseModule({} as any);
expect(await module.data).toBe(exampleData);
});
});
-27
View File
@@ -1,27 +0,0 @@
/**
* @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 {NgDevConfig} from '../../utils/config';
import {AuthenticatedGitClient} from '../../utils/git/authenticated-git-client';
import {CaretakerConfig} from '../config';
/** The BaseModule to extend modules for caretaker checks from. */
export abstract class BaseModule<Data> {
/** The singleton instance of the authenticated git client. */
protected git = AuthenticatedGitClient.get();
/** The data for the module. */
readonly data = this.retrieveData();
constructor(protected config: NgDevConfig<{caretaker: CaretakerConfig}>) {}
/** Asyncronously retrieve data for the module. */
protected abstract retrieveData(): Promise<Data>;
/** Print the information discovered for the module to the terminal. */
abstract printToTerminal(): Promise<void>;
}
-39
View File
@@ -1,39 +0,0 @@
/**
* @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 {getCaretakerConfig} from '../config';
import {CiModule} from './ci';
import {G3Module} from './g3';
import {GithubQueriesModule} from './github';
import {ServicesModule} from './services';
/** List of modules checked for the caretaker check command. */
const moduleList = [
GithubQueriesModule,
ServicesModule,
CiModule,
G3Module,
];
/** Check the status of services which Angular caretakers need to monitor. */
export async function checkServiceStatuses() {
/** The configuration for the caretaker commands. */
const config = getCaretakerConfig();
/** List of instances of Caretaker Check modules */
const caretakerCheckModules = moduleList.map(module => new module(config));
// Module's `data` is casted as Promise<unknown> because the data types of the `module`'s `data`
// promises do not match typings, however our usage here is only to determine when the promise
// resolves.
await Promise.all(caretakerCheckModules.map(module => module.data as Promise<unknown>));
for (const module of caretakerCheckModules) {
await module.printToTerminal();
}
}
-117
View File
@@ -1,117 +0,0 @@
/**
* @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 {SemVer} from 'semver';
import {ReleaseTrain} from '../../release/versioning';
import * as versioning from '../../release/versioning/active-release-trains';
import * as console from '../../utils/console';
import {installVirtualGitClientSpies, mockNgDevConfig} from '../../utils/testing';
import {CiModule} from './ci';
describe('CiModule', () => {
let fetchActiveReleaseTrainsSpy: jasmine.Spy;
let getBranchStatusFromCiSpy: jasmine.Spy;
let infoSpy: jasmine.Spy;
let debugSpy: jasmine.Spy;
beforeEach(() => {
installVirtualGitClientSpies();
fetchActiveReleaseTrainsSpy = spyOn(versioning, 'fetchActiveReleaseTrains');
getBranchStatusFromCiSpy = spyOn(CiModule.prototype, 'getBranchStatusFromCi' as any);
infoSpy = spyOn(console, 'info');
debugSpy = spyOn(console, 'debug');
});
describe('getting data for active trains', () => {
it('handles active rc train', async () => {
const trains = buildMockActiveReleaseTrains(true);
fetchActiveReleaseTrainsSpy.and.resolveTo(trains);
const module = new CiModule({caretaker: {}, ...mockNgDevConfig});
await module.data;
expect(getBranchStatusFromCiSpy).toHaveBeenCalledWith(trains.releaseCandidate.branchName);
expect(getBranchStatusFromCiSpy).toHaveBeenCalledWith(trains.latest.branchName);
expect(getBranchStatusFromCiSpy).toHaveBeenCalledWith(trains.next.branchName);
expect(getBranchStatusFromCiSpy).toHaveBeenCalledTimes(3);
});
it('handles an inactive rc train', async () => {
const trains = buildMockActiveReleaseTrains(false);
fetchActiveReleaseTrainsSpy.and.resolveTo(trains);
const module = new CiModule({caretaker: {}, ...mockNgDevConfig});
await module.data;
expect(getBranchStatusFromCiSpy).toHaveBeenCalledWith(trains.latest.branchName);
expect(getBranchStatusFromCiSpy).toHaveBeenCalledWith(trains.next.branchName);
expect(getBranchStatusFromCiSpy).toHaveBeenCalledTimes(2);
});
it('aggregates information into a useful structure', async () => {
const trains = buildMockActiveReleaseTrains(false);
fetchActiveReleaseTrainsSpy.and.resolveTo(trains);
getBranchStatusFromCiSpy.and.returnValue('success');
const module = new CiModule({caretaker: {}, ...mockNgDevConfig});
const data = await module.data;
expect(data[0]).toEqual(
{active: false, name: 'releaseCandidate', label: '', status: 'not found'});
expect(data[1]).toEqual({
active: true,
name: 'latest-branch',
label: 'latest (latest-branch)',
status: 'success',
});
});
});
it('prints the data retrieved', async () => {
const fakeData = Promise.resolve([
{
active: true,
name: 'name0',
label: 'label0',
status: 'success',
},
{
active: false,
name: 'name1',
label: 'label1',
status: 'failed',
},
]);
fetchActiveReleaseTrainsSpy.and.resolveTo([]);
const module = new CiModule({caretaker: {}, ...mockNgDevConfig});
Object.defineProperty(module, 'data', {value: fakeData});
await module.printToTerminal();
expect(debugSpy).toHaveBeenCalledWith('No active release train for name1');
expect(infoSpy).toHaveBeenCalledWith('label0 ✅');
});
});
/** Build a mock set of ActiveReleaseTrains. */
function buildMockActiveReleaseTrains(withRc: false): versioning.ActiveReleaseTrains&
{releaseCandidate: null};
function buildMockActiveReleaseTrains(withRc: true): versioning.ActiveReleaseTrains&
{releaseCandidate: ReleaseTrain};
function buildMockActiveReleaseTrains(withRc: boolean): versioning.ActiveReleaseTrains {
const baseResult = {
isMajor: false,
version: new SemVer('0.0.0'),
};
return {
releaseCandidate: withRc ? {branchName: 'rc-branch', ...baseResult} : null,
latest: {branchName: 'latest-branch', ...baseResult},
next: {branchName: 'next-branch', ...baseResult}
};
}
-88
View File
@@ -1,88 +0,0 @@
/**
* @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 fetch from 'node-fetch';
import {fetchActiveReleaseTrains, ReleaseTrain} from '../../release/versioning/index';
import {bold, debug, info} from '../../utils/console';
import {BaseModule} from './base';
/** The result of checking a branch on CI. */
type CiBranchStatus = 'success'|'failed'|'not found';
/** A list of results for checking CI branches. */
type CiData = {
active: boolean,
name: string,
label: string,
status: CiBranchStatus,
}[];
export class CiModule extends BaseModule<CiData> {
override async retrieveData() {
const gitRepoWithApi = {api: this.git.github, ...this.git.remoteConfig};
const releaseTrains = await fetchActiveReleaseTrains(gitRepoWithApi);
const ciResultPromises = Object.entries(releaseTrains).map(async ([trainName, train]: [
string, ReleaseTrain|null
]) => {
if (train === null) {
return {
active: false,
name: trainName,
label: '',
status: 'not found' as const,
};
}
return {
active: true,
name: train.branchName,
label: `${trainName} (${train.branchName})`,
status: await this.getBranchStatusFromCi(train.branchName),
};
});
return await Promise.all(ciResultPromises);
}
override async printToTerminal() {
const data = await this.data;
const minLabelLength = Math.max(...data.map(result => result.label.length));
info.group(bold(`CI`));
data.forEach(result => {
if (result.active === false) {
debug(`No active release train for ${result.name}`);
return;
}
const label = result.label.padEnd(minLabelLength);
if (result.status === 'not found') {
info(`${result.name} was not found on CircleCI`);
} else if (result.status === 'success') {
info(`${label} ✅`);
} else {
info(`${label} ❌`);
}
});
info.groupEnd();
info();
}
/** Get the CI status of a given branch from CircleCI. */
private async getBranchStatusFromCi(branch: string): Promise<CiBranchStatus> {
const {owner, name} = this.git.remoteConfig;
const url = `https://circleci.com/gh/${owner}/${name}/tree/${branch}.svg?style=shield`;
const result = await fetch(url).then(result => result.text());
if (result && !result.includes('no builds')) {
return result.includes('passing') ? 'success' : 'failed';
}
return 'not found';
}
}
-36
View File
@@ -1,36 +0,0 @@
/**
* @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 {Argv, CommandModule} from 'yargs';
import {addGithubTokenOption} from '../../utils/git/github-yargs';
import {checkServiceStatuses} from './check';
export interface CaretakerCheckOptions {
githubToken: string;
}
/** Builds the command. */
function builder(yargs: Argv) {
return addGithubTokenOption(yargs);
}
/** Handles the command. */
async function handler() {
await checkServiceStatuses();
}
/** yargs command module for checking status information for the repository */
export const CheckModule: CommandModule<{}, CaretakerCheckOptions> = {
handler,
builder,
command: 'check',
describe: 'Check the status of information the caretaker manages for the repository',
};
-109
View File
@@ -1,109 +0,0 @@
/**
* @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 {SpawnSyncReturns} from 'child_process';
import * as console from '../../utils/console';
import {GitClient} from '../../utils/git/git-client';
import {installVirtualGitClientSpies, mockNgDevConfig} from '../../utils/testing';
import {G3Module, G3StatsData} from './g3';
describe('G3Module', () => {
let getG3FileIncludeAndExcludeLists: jasmine.Spy;
let getLatestShas: jasmine.Spy;
let getDiffStats: jasmine.Spy;
let infoSpy: jasmine.Spy;
beforeEach(() => {
installVirtualGitClientSpies();
getG3FileIncludeAndExcludeLists =
spyOn(G3Module.prototype, 'getG3FileIncludeAndExcludeLists' as any).and.returnValue(null);
getLatestShas = spyOn(G3Module.prototype, 'getLatestShas' as any).and.returnValue(null);
getDiffStats = spyOn(G3Module.prototype, 'getDiffStats' as any).and.returnValue(null);
infoSpy = spyOn(console, 'info');
});
describe('gathering stats', () => {
it('unless the g3 merge config is not defined in the angular robot file', async () => {
getG3FileIncludeAndExcludeLists.and.returnValue(null);
getLatestShas.and.returnValue({g3: 'abc123', master: 'zxy987'});
const module = new G3Module({caretaker: {}, ...mockNgDevConfig});
expect(getDiffStats).not.toHaveBeenCalled();
expect(await module.data).toBe(undefined);
});
it('unless the branch shas are not able to be retrieved', async () => {
getLatestShas.and.returnValue(null);
getG3FileIncludeAndExcludeLists.and.returnValue({include: ['file1'], exclude: []});
const module = new G3Module({caretaker: {}, ...mockNgDevConfig});
expect(getDiffStats).not.toHaveBeenCalled();
expect(await module.data).toBe(undefined);
});
it('for the files which are being synced to g3', async () => {
getLatestShas.and.returnValue({g3: 'abc123', master: 'zxy987'});
getG3FileIncludeAndExcludeLists.and.returnValue({include: ['project1/*'], exclude: []});
getDiffStats.and.callThrough();
spyOn(GitClient.prototype, 'run').and.callFake((args: string[]): any => {
const output: Partial<SpawnSyncReturns<string>> = {};
if (args[0] === 'rev-list') {
output.stdout = '3';
}
if (args[0] === 'diff') {
output.stdout = '5\t6\tproject1/file1\n2\t3\tproject2/file2\n7\t1\tproject1/file3\n';
}
return output;
});
const module = new G3Module({caretaker: {}, ...mockNgDevConfig});
const {insertions, deletions, files, commits} = (await module.data) as G3StatsData;
expect(insertions).toBe(12);
expect(deletions).toBe(7);
expect(files).toBe(2);
expect(commits).toBe(3);
});
});
describe('printing the data retrieved', () => {
it('if files are discovered needing to sync', async () => {
const fakeData = Promise.resolve({
insertions: 25,
deletions: 10,
files: 2,
commits: 2,
});
const module = new G3Module({caretaker: {}, ...mockNgDevConfig});
Object.defineProperty(module, 'data', {value: fakeData});
await module.printToTerminal();
expect(infoSpy).toHaveBeenCalledWith(
'2 files changed, 25 insertions(+), 10 deletions(-) from 2 commits will be included in the next sync');
});
it('if no files need to sync', async () => {
const fakeData = Promise.resolve({
insertions: 0,
deletions: 0,
files: 0,
commits: 25,
});
const module = new G3Module({caretaker: {}, ...mockNgDevConfig});
Object.defineProperty(module, 'data', {value: fakeData});
await module.printToTerminal();
expect(infoSpy).toHaveBeenCalledWith('25 commits between g3 and master');
expect(infoSpy).toHaveBeenCalledWith('✅ No sync is needed at this time');
});
});
});
-156
View File
@@ -1,156 +0,0 @@
/**
* @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 {existsSync, readFileSync} from 'fs';
import * as multimatch from 'multimatch';
import {join} from 'path';
import {parse as parseYaml} from 'yaml';
import {bold, debug, info} from '../../utils/console';
import {BaseModule} from './base';
/** Information expressing the difference between the master and g3 branches */
export interface G3StatsData {
insertions: number;
deletions: number;
files: number;
commits: number;
}
export class G3Module extends BaseModule<G3StatsData|void> {
override async retrieveData() {
const toCopyToG3 = this.getG3FileIncludeAndExcludeLists();
const latestSha = this.getLatestShas();
if (toCopyToG3 === null || latestSha === null) {
return;
}
return this.getDiffStats(
latestSha.g3, latestSha.master, toCopyToG3.include, toCopyToG3.exclude);
}
override async printToTerminal() {
const stats = await this.data;
if (!stats) {
return;
}
info.group(bold('g3 branch check'));
if (stats.files === 0) {
info(`${stats.commits} commits between g3 and master`);
info('✅ No sync is needed at this time');
} else {
info(
`${stats.files} files changed, ${stats.insertions} insertions(+), ${stats.deletions} ` +
`deletions(-) from ${stats.commits} commits will be included in the next sync`);
}
info.groupEnd();
info();
}
/** Fetch and retrieve the latest sha for a specific branch. */
private getShaForBranchLatest(branch: string) {
const {owner, name} = this.git.remoteConfig;
/** The result fo the fetch command. */
const fetchResult =
this.git.runGraceful(['fetch', '-q', `https://github.com/${owner}/${name}.git`, branch]);
if (fetchResult.status !== 0 &&
fetchResult.stderr.includes(`couldn't find remote ref ${branch}`)) {
debug(`No '${branch}' branch exists on upstream, skipping.`);
return null;
}
return this.git.runGraceful(['rev-parse', 'FETCH_HEAD']).stdout.trim();
}
/**
* Get git diff stats between master and g3, for all files and filtered to only g3 affecting
* files.
*/
private getDiffStats(
g3Ref: string, masterRef: string, includeFiles: string[], excludeFiles: string[]) {
/** The diff stats to be returned. */
const stats = {
insertions: 0,
deletions: 0,
files: 0,
commits: 0,
};
// Determine the number of commits between master and g3 refs. */
stats.commits =
parseInt(this.git.run(['rev-list', '--count', `${g3Ref}..${masterRef}`]).stdout, 10);
// Get the numstat information between master and g3
this.git.run(['diff', `${g3Ref}...${masterRef}`, '--numstat'])
.stdout
// Remove the extra space after git's output.
.trim()
// Split each line of git output into array
.split('\n')
// Split each line from the git output into components parts: insertions,
// deletions and file name respectively
.map(line => line.trim().split('\t'))
// Parse number value from the insertions and deletions values
// Example raw line input:
// 10\t5\tsrc/file/name.ts
.map(line => [Number(line[0]), Number(line[1]), line[2]] as [number, number, string])
// Add each line's value to the diff stats, and conditionally to the g3
// stats as well if the file name is included in the files synced to g3.
.forEach(([insertions, deletions, fileName]) => {
if (this.checkMatchAgainstIncludeAndExclude(fileName, includeFiles, excludeFiles)) {
stats.insertions += insertions;
stats.deletions += deletions;
stats.files += 1;
}
});
return stats;
}
/** Determine whether the file name passes both include and exclude checks. */
private checkMatchAgainstIncludeAndExclude(file: string, includes: string[], excludes: string[]) {
return (
multimatch.call(undefined, file, includes).length >= 1 &&
multimatch.call(undefined, file, excludes).length === 0);
}
private getG3FileIncludeAndExcludeLists() {
const angularRobotFilePath = join(this.git.baseDir, '.github/angular-robot.yml');
if (!existsSync(angularRobotFilePath)) {
debug('No angular robot configuration file exists, skipping.');
return null;
}
/** The configuration defined for the angular robot. */
const robotConfig = parseYaml(readFileSync(angularRobotFilePath).toString());
/** The files to be included in the g3 sync. */
const include: string[] = robotConfig?.merge?.g3Status?.include || [];
/** The files to be expected in the g3 sync. */
const exclude: string[] = robotConfig?.merge?.g3Status?.exclude || [];
if (include.length === 0 && exclude.length === 0) {
debug('No g3Status include or exclude lists are defined in the angular robot configuration');
return null;
}
return {include, exclude};
}
private getLatestShas() {
/** The latest sha for the g3 branch. */
const g3 = this.getShaForBranchLatest('g3');
/** The latest sha for the master branch. */
const master = this.getShaForBranchLatest('master');
if (g3 === null || master === null) {
debug('Either the g3 or master was unable to be retrieved');
return null;
}
return {g3, master};
}
}
-119
View File
@@ -1,119 +0,0 @@
/**
* @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 console from '../../utils/console';
import {AuthenticatedGithubClient, GithubClient} from '../../utils/git/github';
import {installVirtualGitClientSpies, mockNgDevConfig} from '../../utils/testing';
import {GithubQueriesModule} from './github';
describe('GithubQueriesModule', () => {
let githubApiSpy: jasmine.Spy;
let infoSpy: jasmine.Spy;
let infoGroupSpy: jasmine.Spy;
beforeEach(() => {
githubApiSpy = spyOn(AuthenticatedGithubClient.prototype, 'graphql')
.and.throwError(
'The graphql query response must always be manually defined in a test.');
installVirtualGitClientSpies();
infoGroupSpy = spyOn(console.info, 'group');
infoSpy = spyOn(console, 'info');
});
describe('gathering stats', () => {
it('unless githubQueries are `undefined`', async () => {
const module =
new GithubQueriesModule({...mockNgDevConfig, caretaker: {githubQueries: undefined}});
expect(await module.data).toBe(undefined);
});
it('unless githubQueries are an empty array', async () => {
const module = new GithubQueriesModule({...mockNgDevConfig, caretaker: {githubQueries: []}});
expect(await module.data).toBe(undefined);
});
it('for the requested Github queries', async () => {
githubApiSpy.and.returnValue({
'keynamewithspaces': {issueCount: 1, nodes: [{url: 'http://github.com/owner/name/issue/1'}]}
});
const module = new GithubQueriesModule({
...mockNgDevConfig,
caretaker: {githubQueries: [{name: 'key name with spaces', query: 'issue: yes'}]}
});
expect(await module.data).toEqual([{
queryName: 'key name with spaces',
count: 1,
queryUrl: 'https://github.com/owner/name/issues?q=issue:%20yes',
matchedUrls: ['http://github.com/owner/name/issue/1'],
}]);
});
});
describe('printing the data retrieved', () => {
it('if there are no matches of the query', async () => {
const fakeData = Promise.resolve([
{
queryName: 'query1',
count: 0,
queryUrl: 'https://github.com/owner/name/issues?q=issue:%20no',
matchedUrls: [],
},
{
queryName: 'query2',
count: 0,
queryUrl: 'https://github.com/owner/name/issues?q=something',
matchedUrls: [],
},
]);
const module = new GithubQueriesModule({caretaker: {}, ...mockNgDevConfig});
Object.defineProperty(module, 'data', {value: fakeData});
await module.printToTerminal();
expect(infoGroupSpy).toHaveBeenCalledWith('Github Tasks');
expect(infoSpy).toHaveBeenCalledWith('query1 0');
expect(infoSpy).toHaveBeenCalledWith('query2 0');
});
it('if there are maches of the query', async () => {
const fakeData = Promise.resolve([
{
queryName: 'query1',
count: 1,
queryUrl: 'https://github.com/owner/name/issues?q=issue:%20yes',
matchedUrls: ['http://github.com/owner/name/issue/1'],
},
{
queryName: 'query2',
count: 0,
queryUrl: 'https://github.com/owner/name/issues?q=something',
matchedUrls: [],
},
]);
const module = new GithubQueriesModule({caretaker: {}, ...mockNgDevConfig});
Object.defineProperty(module, 'data', {value: fakeData});
await module.printToTerminal();
expect(infoGroupSpy).toHaveBeenCalledWith('Github Tasks');
expect(infoSpy).toHaveBeenCalledWith('query1 1');
expect(infoGroupSpy)
.toHaveBeenCalledWith('https://github.com/owner/name/issues?q=issue:%20yes');
expect(infoSpy).toHaveBeenCalledWith('- http://github.com/owner/name/issue/1');
expect(infoSpy).toHaveBeenCalledWith('query2 0');
});
});
});
-120
View File
@@ -1,120 +0,0 @@
/**
* @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 {alias, onUnion, params, types} from 'typed-graphqlify';
import {bold, debug, info} from '../../utils/console';
import {CaretakerConfig} from '../config';
import {BaseModule} from './base';
/** A list of generated results for a github query. */
type GithubQueryResults = {
queryName: string,
count: number,
queryUrl: string,
matchedUrls: string[],
}[];
/** The fragment for a result from Github's api for a Github query. */
const GithubQueryResultFragment = {
issueCount: types.number,
nodes: [{...onUnion({
PullRequest: {
url: types.string,
},
Issue: {
url: types.string,
},
})}],
};
/** An object containing results of multiple queries. */
type GithubQueryResult = {
[key: string]: typeof GithubQueryResultFragment;
};
/**
* Cap the returned issues in the queries to an arbitrary 20. At that point, caretaker has a lot
* of work to do and showing more than that isn't really useful.
*/
const MAX_RETURNED_ISSUES = 20;
export class GithubQueriesModule extends BaseModule<GithubQueryResults|void> {
override async retrieveData() {
// Non-null assertion is used here as the check for undefined immediately follows to confirm the
// assertion. Typescript's type filtering does not seem to work as needed to understand
// whether githubQueries is undefined or not.
let queries = this.config.caretaker?.githubQueries!;
if (queries === undefined || queries.length === 0) {
debug('No github queries defined in the configuration, skipping');
return;
}
/** The results of the generated github query. */
const queryResult = await this.git.github.graphql(this.buildGraphqlQuery(queries));
const results = Object.values(queryResult);
const {owner, name: repo} = this.git.remoteConfig;
return results.map((result, i) => {
return {
queryName: queries[i].name,
count: result.issueCount,
queryUrl: encodeURI(`https://github.com/${owner}/${repo}/issues?q=${queries[i].query}`),
matchedUrls: result.nodes.map(node => node.url)
};
});
}
/** Build a Graphql query statement for the provided queries. */
private buildGraphqlQuery(queries: NonNullable<CaretakerConfig['githubQueries']>) {
/** The query object for graphql. */
const graphqlQuery: GithubQueryResult = {};
const {owner, name: repo} = this.git.remoteConfig;
/** The Github search filter for the configured repository. */
const repoFilter = `repo:${owner}/${repo}`;
queries.forEach(({name, query}) => {
/** The name of the query, with spaces removed to match Graphql requirements. */
const queryKey = alias(name.replace(/ /g, ''), 'search');
graphqlQuery[queryKey] = params(
{
type: 'ISSUE',
first: MAX_RETURNED_ISSUES,
query: `"${repoFilter} ${query.replace(/"/g, '\\"')}"`,
},
{...GithubQueryResultFragment});
});
return graphqlQuery;
}
override async printToTerminal() {
const queryResults = await this.data;
if (!queryResults) {
return;
}
info.group(bold('Github Tasks'));
const minQueryNameLength = Math.max(...queryResults.map(result => result.queryName.length));
for (const queryResult of queryResults) {
info(`${queryResult.queryName.padEnd(minQueryNameLength)} ${queryResult.count}`);
if (queryResult.count > 0) {
info.group(queryResult.queryUrl);
queryResult.matchedUrls.forEach(url => info(`- ${url}`));
if (queryResult.count > MAX_RETURNED_ISSUES) {
info(`... ${queryResult.count - MAX_RETURNED_ISSUES} additional matches`);
}
info.groupEnd();
}
}
info.groupEnd();
info();
}
}
@@ -1,68 +0,0 @@
/**
* @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 console from '../../utils/console';
import {installVirtualGitClientSpies, mockNgDevConfig} from '../../utils/testing';
import {services, ServicesModule} from './services';
describe('ServicesModule', () => {
let getStatusFromStandardApiSpy: jasmine.Spy;
let infoSpy: jasmine.Spy;
let infoGroupSpy: jasmine.Spy;
services.splice(0, Infinity, {url: 'fakeStatus.com/api.json', name: 'Service Name'});
beforeEach(() => {
getStatusFromStandardApiSpy = spyOn(ServicesModule.prototype, 'getStatusFromStandardApi');
installVirtualGitClientSpies();
infoGroupSpy = spyOn(console.info, 'group');
infoSpy = spyOn(console, 'info');
});
describe('gathering status', () => {
it('for each of the services', async () => {
new ServicesModule({caretaker: {}, ...mockNgDevConfig});
expect(getStatusFromStandardApiSpy)
.toHaveBeenCalledWith({url: 'fakeStatus.com/api.json', name: 'Service Name'});
});
});
describe('printing the data retrieved', () => {
it('for each service ', async () => {
const fakeData = Promise.resolve([
{
name: 'Service 1',
status: 'passing',
description: 'Everything is working great',
lastUpdated: new Date(0),
},
{
name: 'Service 2',
status: 'failing',
description: 'Literally everything is broken',
lastUpdated: new Date(0),
},
]);
const module = new ServicesModule({caretaker: {}, ...mockNgDevConfig});
Object.defineProperty(module, 'data', {value: fakeData});
await module.printToTerminal();
expect(infoGroupSpy).toHaveBeenCalledWith('Service Statuses');
expect(infoSpy).toHaveBeenCalledWith('Service 1 ✅');
expect(infoGroupSpy)
.toHaveBeenCalledWith(`Service 2 ❌ (Updated: ${new Date(0).toLocaleString()})`);
expect(infoSpy).toHaveBeenCalledWith(' Details: Literally everything is broken');
});
});
});
-81
View File
@@ -1,81 +0,0 @@
/**
* @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 fetch from 'node-fetch';
import {bold, info} from '../../utils/console';
import {BaseModule} from './base';
interface ServiceConfig {
name: string;
url: string;
}
/** The results of checking the status of a service */
interface StatusCheckResult {
name: string;
status: 'passing'|'failing';
description: string;
lastUpdated: Date;
}
/** List of services Angular relies on. */
export const services: ServiceConfig[] = [
{
url: 'https://status.us-west-1.saucelabs.com/api/v2/status.json',
name: 'Saucelabs',
},
{
url: 'https://status.npmjs.org/api/v2/status.json',
name: 'Npm',
},
{
url: 'https://status.circleci.com/api/v2/status.json',
name: 'CircleCi',
},
{
url: 'https://www.githubstatus.com/api/v2/status.json',
name: 'Github',
},
];
export class ServicesModule extends BaseModule<StatusCheckResult[]> {
override async retrieveData() {
return Promise.all(services.map(service => this.getStatusFromStandardApi(service)));
}
override async printToTerminal() {
const statuses = await this.data;
const serviceNameMinLength = Math.max(...statuses.map(service => service.name.length));
info.group(bold('Service Statuses'));
for (const status of statuses) {
const name = status.name.padEnd(serviceNameMinLength);
if (status.status === 'passing') {
info(`${name} ✅`);
} else {
info.group(`${name} ❌ (Updated: ${status.lastUpdated.toLocaleString()})`);
info(` Details: ${status.description}`);
info.groupEnd();
}
}
info.groupEnd();
info();
}
/** Retrieve the status information for a service which uses a standard API response. */
async getStatusFromStandardApi(service: ServiceConfig): Promise<StatusCheckResult> {
const result = await fetch(service.url).then(result => result.json());
const status = result.status.indicator === 'none' ? 'passing' : 'failing';
return {
name: service.name,
status,
description: result.status.description,
lastUpdated: new Date(result.page.updated_at)
};
}
}
-17
View File
@@ -1,17 +0,0 @@
/**
* @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 {Argv} from 'yargs';
import {CheckModule} from './check/cli';
import {HandoffModule} from './handoff/cli';
/** Build the parser for the caretaker commands. */
export function buildCaretakerParser(yargs: Argv) {
return yargs.command(CheckModule).command(HandoffModule);
}
-30
View File
@@ -1,30 +0,0 @@
/**
* @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 {assertNoErrors, getConfig, NgDevConfig} from '../utils/config';
export interface CaretakerConfig {
/** Github queries showing a snapshot of pulls/issues caretakers need to monitor. */
githubQueries?: {name: string; query: string;}[];
/**
* The Github group used to track current caretakers. A second group is assumed to exist with the
* name "<group-name>-roster" containing a list of all users eligible for the caretaker group.
* */
caretakerGroup?: string;
}
/** Retrieve and validate the config as `CaretakerConfig`. */
export function getCaretakerConfig() {
// List of errors encountered validating the config.
const errors: string[] = [];
// The non-validated config object.
const config: Partial<NgDevConfig<{caretaker: CaretakerConfig}>> = getConfig();
assertNoErrors(errors);
return config as Required<typeof config>;
}
-36
View File
@@ -1,36 +0,0 @@
/**
* @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 {Argv, CommandModule} from 'yargs';
import {addGithubTokenOption} from '../../utils/git/github-yargs';
import {updateCaretakerTeamViaPrompt} from './update-github-team';
export interface CaretakerHandoffOptions {
githubToken: string;
}
/** Builds the command. */
function builder(yargs: Argv) {
return addGithubTokenOption(yargs);
}
/** Handles the command. */
async function handler() {
await updateCaretakerTeamViaPrompt();
}
/** yargs command module for assisting in handing off caretaker. */
export const HandoffModule: CommandModule<{}, CaretakerHandoffOptions> = {
handler,
builder,
command: 'handoff',
describe: 'Run a handoff assistant to aide in moving to the next caretaker',
};
@@ -1,132 +0,0 @@
/**
* @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 {prompt} from 'inquirer';
import {debug, green, info, red, yellow} from '../../utils/console';
import {AuthenticatedGitClient} from '../../utils/git/authenticated-git-client';
import {getCaretakerConfig} from '../config';
/** Update the Github caretaker group, using a prompt to obtain the new caretaker group members. */
export async function updateCaretakerTeamViaPrompt() {
/** Caretaker specific configuration. */
const caretakerConfig = getCaretakerConfig().caretaker;
if (caretakerConfig.caretakerGroup === undefined) {
throw Error('`caretakerGroup` is not defined in the `caretaker` config');
}
/** The list of current members in the group. */
const current = await getGroupMembers(caretakerConfig.caretakerGroup);
/** The list of members able to be added to the group as defined by a separate roster group. */
const roster = await getGroupMembers(`${caretakerConfig.caretakerGroup}-roster`);
const {
/** The list of users selected to be members of the caretaker group. */
selected,
/** Whether the user positively confirmed the selected made. */
confirm
} =
await prompt([
{
type: 'checkbox',
choices: roster,
message: 'Select 2 caretakers for the upcoming rotation:',
default: current,
name: 'selected',
prefix: '',
validate: (selected: string[]) => {
if (selected.length !== 2) {
return 'Please select exactly 2 caretakers for the upcoming rotation.';
}
return true;
},
},
{
type: 'confirm',
default: true,
prefix: '',
message: 'Are you sure?',
name: 'confirm',
}
]);
if (confirm === false) {
info(yellow(' ⚠ Skipping caretaker group update.'));
return;
}
if (JSON.stringify(selected) === JSON.stringify(current)) {
info(green(' √ Caretaker group already up to date.'));
return;
}
try {
await setCaretakerGroup(caretakerConfig.caretakerGroup, selected);
} catch {
info(red(' ✘ Failed to update caretaker group.'));
return;
}
info(green(' √ Successfully updated caretaker group'));
}
/** Retrieve the current list of members for the provided group. */
async function getGroupMembers(group: string) {
/** The authenticated GitClient instance. */
const git = AuthenticatedGitClient.get();
return (await git.github.teams.listMembersInOrg({
org: git.remoteConfig.owner,
team_slug: group,
}))
.data.filter(_ => !!_)
.map(member => member!.login);
}
async function setCaretakerGroup(group: string, members: string[]) {
/** The authenticated GitClient instance. */
const git = AuthenticatedGitClient.get();
/** The full name of the group <org>/<group name>. */
const fullSlug = `${git.remoteConfig.owner}/${group}`;
/** The list of current members of the group. */
const current = await getGroupMembers(group);
/** The list of users to be removed from the group. */
const removed = current.filter(login => !members.includes(login));
/** Add a user to the group. */
const add = async (username: string) => {
debug(`Adding ${username} to ${fullSlug}.`);
await git.github.teams.addOrUpdateMembershipForUserInOrg({
org: git.remoteConfig.owner,
team_slug: group,
username,
role: 'maintainer',
});
};
/** Remove a user from the group. */
const remove = async (username: string) => {
debug(`Removing ${username} from ${fullSlug}.`);
await git.github.teams.removeMembershipForUserInOrg({
org: git.remoteConfig.owner,
team_slug: group,
username,
});
};
debug.group(`Caretaker Group: ${fullSlug}`);
debug(`Current Membership: ${current.join(', ')}`);
debug(`New Membership: ${members.join(', ')}`);
debug(`Removed: ${removed.join(', ')}`);
debug.groupEnd();
// Add members before removing to prevent the account performing the action from removing their
// permissions to change the group membership early.
await Promise.all(members.map(add));
await Promise.all(removed.map(remove));
debug(`Successfuly updated ${fullSlug}`);
}
-37
View File
@@ -1,37 +0,0 @@
#!/usr/bin/env node
/**
* @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 {buildCaretakerParser} from './caretaker/cli';
import {buildCommitMessageParser} from './commit-message/cli';
import {buildFormatParser} from './format/cli';
import {buildNgbotParser} from './ngbot/cli';
import {buildPrParser} from './pr/cli';
import {buildPullapproveParser} from './pullapprove/cli';
import {buildReleaseParser} from './release/cli';
import {tsCircularDependenciesBuilder} from './ts-circular-dependencies/index';
import {captureLogOutputForCommand} from './utils/console';
import {buildMiscParser} from './misc/cli';
yargs.scriptName('ng-dev')
.middleware(captureLogOutputForCommand)
.demandCommand()
.recommendCommands()
.command('commit-message <command>', '', buildCommitMessageParser)
.command('format <command>', '', buildFormatParser)
.command('pr <command>', '', buildPrParser)
.command('pullapprove <command>', '', buildPullapproveParser)
.command('release <command>', '', buildReleaseParser)
.command('ts-circular-deps <command>', '', tsCircularDependenciesBuilder)
.command('caretaker <command>', '', buildCaretakerParser)
.command('misc <command>', '', buildMiscParser)
.command('ngbot <command>', false, buildNgbotParser)
.wrap(120)
.strict()
.parse();
-1
View File
@@ -1 +0,0 @@
pr/merge/strategies/commit-message-filter.js
-43
View File
@@ -1,43 +0,0 @@
load("//dev-infra:defaults.bzl", "jasmine_node_test", "ts_library")
ts_library(
name = "commit-message",
srcs = glob(
["**/*.ts"],
exclude = ["**/*.spec.ts"],
),
visibility = ["//dev-infra:__subpackages__"],
deps = [
"//dev-infra/utils",
"@npm//@types/conventional-commits-parser",
"@npm//@types/git-raw-commits",
"@npm//@types/inquirer",
"@npm//@types/node",
"@npm//@types/yargs",
"@npm//conventional-commits-parser",
"@npm//git-raw-commits",
"@npm//inquirer",
"@npm//yargs",
],
)
ts_library(
name = "test_lib",
testonly = True,
srcs = glob(["**/*.spec.ts"]),
deps = [
":commit-message",
"//dev-infra/utils",
"@npm//@types/events",
"@npm//@types/jasmine",
"@npm//@types/node",
"@npm//inquirer",
],
)
jasmine_node_test(
name = "test",
deps = [
":test_lib",
],
)
-21
View File
@@ -1,21 +0,0 @@
/**
* @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 {RestoreCommitMessageModule} from './restore-commit-message/cli';
import {ValidateFileModule} from './validate-file/cli';
import {ValidateRangeModule} from './validate-range/cli';
/** Build the parser for the commit-message commands. */
export function buildCommitMessageParser(localYargs: yargs.Argv) {
return localYargs.help()
.strict()
.command(RestoreCommitMessageModule)
.command(ValidateFileModule)
.command(ValidateRangeModule);
}
-110
View File
@@ -1,110 +0,0 @@
/**
* @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 {assertNoErrors, getConfig, NgDevConfig} from '../utils/config';
/** Configuration for commit-message comands. */
export interface CommitMessageConfig {
maxLineLength: number;
minBodyLength: number;
minBodyLengthTypeExcludes?: string[];
scopes: string[];
}
/** Retrieve and validate the config as `CommitMessageConfig`. */
export function getCommitMessageConfig() {
// List of errors encountered validating the config.
const errors: string[] = [];
// The non-validated config object.
const config: Partial<NgDevConfig<{commitMessage: CommitMessageConfig}>> = getConfig();
if (config.commitMessage === undefined) {
errors.push(`No configuration defined for "commitMessage"`);
}
assertNoErrors(errors);
return config as Required<typeof config>;
}
/** Scope requirement level to be set for each commit type. */
export enum ScopeRequirement {
Required,
Optional,
Forbidden,
}
export enum ReleaseNotesLevel {
Hidden,
Visible,
}
/** A commit type */
export interface CommitType {
description: string;
name: string;
scope: ScopeRequirement;
releaseNotesLevel: ReleaseNotesLevel;
}
/** The valid commit types for Angular commit messages. */
export const COMMIT_TYPES: {[key: string]: CommitType} = {
build: {
name: 'build',
description: 'Changes to local repository build system and tooling',
scope: ScopeRequirement.Optional,
releaseNotesLevel: ReleaseNotesLevel.Hidden,
},
ci: {
name: 'ci',
description: 'Changes to CI configuration and CI specific tooling',
scope: ScopeRequirement.Forbidden,
releaseNotesLevel: ReleaseNotesLevel.Hidden,
},
docs: {
name: 'docs',
description: 'Changes which exclusively affects documentation.',
scope: ScopeRequirement.Optional,
releaseNotesLevel: ReleaseNotesLevel.Hidden,
},
feat: {
name: 'feat',
description: 'Creates a new feature',
scope: ScopeRequirement.Required,
releaseNotesLevel: ReleaseNotesLevel.Visible,
},
fix: {
name: 'fix',
description: 'Fixes a previously discovered failure/bug',
scope: ScopeRequirement.Required,
releaseNotesLevel: ReleaseNotesLevel.Visible,
},
perf: {
name: 'perf',
description: 'Improves performance without any change in functionality or API',
scope: ScopeRequirement.Required,
releaseNotesLevel: ReleaseNotesLevel.Visible,
},
refactor: {
name: 'refactor',
description: 'Refactor without any change in functionality or API (includes style changes)',
scope: ScopeRequirement.Optional,
releaseNotesLevel: ReleaseNotesLevel.Hidden,
},
release: {
name: 'release',
description: 'A release point in the repository',
scope: ScopeRequirement.Forbidden,
releaseNotesLevel: ReleaseNotesLevel.Hidden,
},
test: {
name: 'test',
description: 'Improvements or corrections made to the project\'s test suite',
scope: ScopeRequirement.Optional,
releaseNotesLevel: ReleaseNotesLevel.Hidden,
},
};
-190
View File
@@ -1,190 +0,0 @@
/**
* @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 {parseCommitMessage} from './parse';
import {commitMessageBuilder, CommitMessageParts} from './test-util';
const commitValues: CommitMessageParts = {
prefix: '',
type: 'fix',
npmScope: '',
scope: 'changed-area',
summary: 'This is a short summary of the change',
body: 'This is a longer description of the change',
footer: 'Closes #1',
};
const buildCommitMessage = commitMessageBuilder(commitValues);
describe('commit message parsing:', () => {
describe('parses the scope', () => {
it('when only a scope is defined', () => {
const message = buildCommitMessage();
expect(parseCommitMessage(message).scope).toBe(commitValues.scope);
expect(parseCommitMessage(message).npmScope).toBe('');
});
it('when an npmScope and scope are defined', () => {
const message = buildCommitMessage({npmScope: 'myNpmPackage'});
expect(parseCommitMessage(message).scope).toBe(commitValues.scope);
expect(parseCommitMessage(message).npmScope).toBe('myNpmPackage');
});
});
it('parses the type', () => {
const message = buildCommitMessage();
expect(parseCommitMessage(message).type).toBe(commitValues.type);
});
it('parses the header', () => {
const message = buildCommitMessage();
expect(parseCommitMessage(message).header)
.toBe(`${commitValues.type}(${commitValues.scope}): ${commitValues.summary}`);
});
it('parses the body', () => {
const message = buildCommitMessage();
expect(parseCommitMessage(message).body).toBe(commitValues.body);
});
it('parses the subject', () => {
const message = buildCommitMessage();
expect(parseCommitMessage(message).subject).toBe(commitValues.summary);
});
it('identifies if a commit is a fixup', () => {
const message1 = buildCommitMessage();
expect(parseCommitMessage(message1).isFixup).toBe(false);
const message2 = buildCommitMessage({prefix: 'fixup! '});
expect(parseCommitMessage(message2).isFixup).toBe(true);
});
it('identifies if a commit is a revert', () => {
const message1 = buildCommitMessage();
expect(parseCommitMessage(message1).isRevert).toBe(false);
const message2 = buildCommitMessage({prefix: 'revert: '});
expect(parseCommitMessage(message2).isRevert).toBe(true);
const message3 = buildCommitMessage({prefix: 'revert '});
expect(parseCommitMessage(message3).isRevert).toBe(true);
});
it('identifies if a commit is a squash', () => {
const message1 = buildCommitMessage();
expect(parseCommitMessage(message1).isSquash).toBe(false);
const message2 = buildCommitMessage({prefix: 'squash! '});
expect(parseCommitMessage(message2).isSquash).toBe(true);
});
it('ignores comment lines', () => {
const message = buildCommitMessage({
prefix: '# This is a comment line before the header.\n' +
'## This is another comment line before the headers.\n',
body: '# This is a comment line befor the body.\n' +
'This is line 1 of the actual body.\n' +
'## This is another comment line inside the body.\n' +
'This is line 2 of the actual body (and it also contains a # but it not a comment).\n' +
'### This is yet another comment line after the body.\n',
});
const parsedMessage = parseCommitMessage(message);
expect(parsedMessage.header)
.toBe(`${commitValues.type}(${commitValues.scope}): ${commitValues.summary}`);
expect(parsedMessage.body)
.toBe(
'This is line 1 of the actual body.\n' +
'This is line 2 of the actual body (and it also contains a # but it not a comment).');
});
describe('parses breaking change notes', () => {
const summary = 'This breaks things';
const description = 'This is how it breaks things.';
it('when only a summary is provided', () => {
const message = buildCommitMessage({
footer: `BREAKING CHANGE: ${summary}`,
});
const parsedMessage = parseCommitMessage(message);
expect(parsedMessage.breakingChanges[0].text).toBe(summary);
expect(parsedMessage.breakingChanges.length).toBe(1);
});
it('when only a description is provided', () => {
const message = buildCommitMessage({
footer: `BREAKING CHANGE:\n\n${description}`,
});
const parsedMessage = parseCommitMessage(message);
expect(parsedMessage.breakingChanges[0].text).toBe(description);
expect(parsedMessage.breakingChanges.length).toBe(1);
});
it('when a summary and description are provied', () => {
const message = buildCommitMessage({
footer: `BREAKING CHANGE: ${summary}\n\n${description}`,
});
const parsedMessage = parseCommitMessage(message);
expect(parsedMessage.breakingChanges[0].text).toBe(`${summary}\n\n${description}`);
expect(parsedMessage.breakingChanges.length).toBe(1);
});
it('only when keyword is at the beginning of a line', () => {
const message = buildCommitMessage({
body: 'This changes how the `BREAKING CHANGE: ` commit message note\n' +
'keyword is detected for the changelog.',
});
const parsedMessage = parseCommitMessage(message);
expect(parsedMessage.breakingChanges.length).toBe(0);
});
});
describe('parses deprecation notes', () => {
const summary = 'This will break things later';
const description = 'This is a long winded explanation of why it \nwill break things later.';
it('when only a summary is provided', () => {
const message = buildCommitMessage({
footer: `DEPRECATED: ${summary}`,
});
const parsedMessage = parseCommitMessage(message);
expect(parsedMessage.deprecations[0].text).toBe(summary);
expect(parsedMessage.deprecations.length).toBe(1);
});
it('when only a description is provided', () => {
const message = buildCommitMessage({
footer: `DEPRECATED:\n\n${description}`,
});
const parsedMessage = parseCommitMessage(message);
expect(parsedMessage.deprecations[0].text).toBe(description);
expect(parsedMessage.deprecations.length).toBe(1);
});
it('when a summary and description are provied', () => {
const message = buildCommitMessage({
footer: `DEPRECATED: ${summary}\n\n${description}`,
});
const parsedMessage = parseCommitMessage(message);
expect(parsedMessage.deprecations[0].text).toBe(`${summary}\n\n${description}`);
expect(parsedMessage.deprecations.length).toBe(1);
});
it('only when keyword is at the beginning of a line', () => {
const message = buildCommitMessage({
body: 'This changes how the `DEPRECATED: ` commit message note\n' +
'keyword is detected for the changelog.',
});
const parsedMessage = parseCommitMessage(message);
expect(parsedMessage.deprecations.length).toBe(0);
});
});
});
-169
View File
@@ -1,169 +0,0 @@
/**
* @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 {Commit as ParsedCommit, Options, sync as parse} from 'conventional-commits-parser';
/** A parsed commit, containing the information needed to validate the commit. */
export interface Commit {
/** The full raw text of the commit. */
fullText: string;
/** The header line of the commit, will be used in the changelog entries. */
header: string;
/** The full body of the commit, not including the footer. */
body: string;
/** The footer of the commit, containing issue references and note sections. */
footer: string;
/** A list of the references to other issues made throughout the commit message. */
references: ParsedCommit.Reference[];
/** The type of the commit message. */
type: string;
/** The scope of the commit message. */
scope: string;
/** The npm scope of the commit message. */
npmScope: string;
/** The subject of the commit message. */
subject: string;
/** A list of breaking change notes in the commit message. */
breakingChanges: ParsedCommit.Note[];
/** A list of deprecation notes in the commit message. */
deprecations: ParsedCommit.Note[];
/** Whether the commit is a fixup commit. */
isFixup: boolean;
/** Whether the commit is a squash commit. */
isSquash: boolean;
/** Whether the commit is a revert commit. */
isRevert: boolean;
}
/** A parsed commit which originated from a Git Log entry */
export interface CommitFromGitLog extends Commit {
author: string;
hash: string;
shortHash: string;
}
/**
* A list of tuples expressing the fields to extract from each commit log entry. The tuple contains
* two values, the first is the key for the property and the second is the template shortcut for the
* git log command.
*/
const commitFields = {
hash: '%H',
shortHash: '%h',
author: '%aN',
};
/** The additional fields to be included in commit log entries for parsing. */
export type CommitFields = typeof commitFields;
/** The commit fields described as git log format entries for parsing. */
export const commitFieldsAsFormat = (fields: CommitFields) => {
return Object.entries(fields).map(([key, value]) => `%n-${key}-%n${value}`).join('');
};
/**
* The git log format template to create git log entries for parsing.
*
* The conventional commits parser expects to parse the standard git log raw body (%B) into its
* component parts. Additionally it will parse additional fields with keys defined by
* `-{key name}-` separated by new lines.
* */
export const gitLogFormatForParsing = `%B${commitFieldsAsFormat(commitFields)}`;
/** Markers used to denote the start of a note section in a commit. */
enum NoteSections {
BREAKING_CHANGE = 'BREAKING CHANGE',
DEPRECATED = 'DEPRECATED',
}
/** Regex determining if a commit is a fixup. */
const FIXUP_PREFIX_RE = /^fixup! /i;
/** Regex determining if a commit is a squash. */
const SQUASH_PREFIX_RE = /^squash! /i;
/** Regex determining if a commit is a revert. */
const REVERT_PREFIX_RE = /^revert:? /i;
/**
* Regex pattern for parsing the header line of a commit.
*
* Several groups are being matched to be used in the parsed commit object, being mapped to the
* `headerCorrespondence` object.
*
* The pattern can be broken down into component parts:
* - `(\w+)` - a capturing group discovering the type of the commit.
* - `(?:\((?:([^/]+)\/)?([^)]+)\))?` - a pair of capturing groups to capture the scope and,
* optionally the npmScope of the commit.
* - `(.*)` - a capturing group discovering the subject of the commit.
*/
const headerPattern = /^(\w+)(?:\((?:([^/]+)\/)?([^)]+)\))?: (.*)$/;
/**
* The property names used for the values extracted from the header via the `headerPattern` regex.
*/
const headerCorrespondence = ['type', 'npmScope', 'scope', 'subject'];
/**
* Configuration options for the commit parser.
*
* NOTE: An extended type from `Options` must be used because the current
* @types/conventional-commits-parser version does not include the `notesPattern` field.
*/
const parseOptions: Options&{notesPattern: (keywords: string) => RegExp} = {
commentChar: '#',
headerPattern,
headerCorrespondence,
noteKeywords: [NoteSections.BREAKING_CHANGE, NoteSections.DEPRECATED],
notesPattern: (keywords: string) => new RegExp(`^\s*(${keywords}): ?(.*)`),
};
/** Parse a commit message into its composite parts. */
export const parseCommitMessage: (fullText: string) => Commit = parseInternal;
/** Parse a commit message from a git log entry into its composite parts. */
export const parseCommitFromGitLog: (fullText: Buffer) => CommitFromGitLog = parseInternal;
/** Parse a full commit message into its composite parts. */
function parseInternal(fullText: string): Commit;
function parseInternal(fullText: Buffer): CommitFromGitLog;
function parseInternal(fullText: string|Buffer): CommitFromGitLog|Commit {
// Ensure the fullText symbol is a `string`, even if a Buffer was provided.
fullText = fullText.toString();
/** The commit message text with the fixup and squash markers stripped out. */
const strippedCommitMsg = fullText.replace(FIXUP_PREFIX_RE, '')
.replace(SQUASH_PREFIX_RE, '')
.replace(REVERT_PREFIX_RE, '');
/** The initially parsed commit. */
const commit = parse(strippedCommitMsg, parseOptions);
/** A list of breaking change notes from the commit. */
const breakingChanges: ParsedCommit.Note[] = [];
/** A list of deprecation notes from the commit. */
const deprecations: ParsedCommit.Note[] = [];
// Extract the commit message notes by marked types into their respective lists.
commit.notes.forEach((note: ParsedCommit.Note) => {
if (note.title === NoteSections.BREAKING_CHANGE) {
return breakingChanges.push(note);
}
if (note.title === NoteSections.DEPRECATED) {
return deprecations.push(note);
}
});
return {
fullText,
breakingChanges,
deprecations,
body: commit.body || '',
footer: commit.footer || '',
header: commit.header || '',
references: commit.references,
scope: commit.scope || '',
subject: commit.subject || '',
type: commit.type || '',
npmScope: commit.npmScope || '',
isFixup: FIXUP_PREFIX_RE.test(fullText),
isSquash: SQUASH_PREFIX_RE.test(fullText),
isRevert: REVERT_PREFIX_RE.test(fullText),
author: commit.author || undefined,
hash: commit.hash || undefined,
shortHash: commit.shortHash || undefined,
};
}
@@ -1,65 +0,0 @@
/**
* @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 {CommitMsgSource} from './commit-message-source';
import {restoreCommitMessage} from './restore-commit-message';
export interface RestoreCommitMessageOptions {
file?: string;
source?: string;
fileEnvVariable?: string;
}
/** Builds the command. */
function builder(yargs: Argv) {
return yargs
.option('file-env-variable' as 'fileEnvVariable', {
type: 'string',
description: 'The key for the environment variable which holds the arguments for the\n' +
'prepare-commit-msg hook as described here:\n' +
'https://git-scm.com/docs/githooks#_prepare_commit_msg'
})
.positional('file', {type: 'string'})
.positional('source', {type: 'string'});
}
/** Handles the command. */
async function handler({fileEnvVariable, file, source}: Arguments<RestoreCommitMessageOptions>) {
// File and source are provided as command line parameters
if (file !== undefined) {
restoreCommitMessage(file, source as CommitMsgSource);
return;
}
// File and source are provided as values held in an environment variable.
if (fileEnvVariable !== undefined) {
const [fileFromEnv, sourceFromEnv] = (process.env[fileEnvVariable!] || '').split(' ');
if (!fileFromEnv) {
throw new Error(`Provided environment variable "${fileEnvVariable}" was not found.`);
}
restoreCommitMessage(fileFromEnv, sourceFromEnv as CommitMsgSource);
return;
}
throw new Error(
'No file path and commit message source provide. Provide values via positional command ' +
'arguments, or via the --file-env-variable flag');
}
/** yargs command module describing the command. */
export const RestoreCommitMessageModule: CommandModule<{}, RestoreCommitMessageOptions> = {
handler,
builder,
command: 'restore-commit-message-draft [file] [source]',
// Description: Restore a commit message draft if one has been saved from a failed commit attempt.
// No describe is defiend to hide the command from the --help.
describe: false,
};
@@ -1,30 +0,0 @@
/**
* @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 {existsSync, readFileSync, unlinkSync, writeFileSync} from 'fs';
/** Load the commit message draft from the file system if it exists. */
export function loadCommitMessageDraft(basePath: string) {
const commitMessageDraftPath = `${basePath}.ngDevSave`;
if (existsSync(commitMessageDraftPath)) {
return readFileSync(commitMessageDraftPath).toString();
}
return '';
}
/** Remove the commit message draft from the file system. */
export function deleteCommitMessageDraft(basePath: string) {
const commitMessageDraftPath = `${basePath}.ngDevSave`;
if (existsSync(commitMessageDraftPath)) {
unlinkSync(commitMessageDraftPath);
}
}
/** Save the commit message draft to the file system for later retrieval. */
export function saveCommitMessageDraft(basePath: string, commitMessage: string) {
writeFileSync(`${basePath}.ngDevSave`, commitMessage);
}
@@ -1,13 +0,0 @@
/**
* @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
*/
/**
* The source triggering the git commit message creation.
* As described in: https://git-scm.com/docs/githooks#_prepare_commit_msg
*/
export type CommitMsgSource = 'message'|'template'|'merge'|'squash'|'commit';
@@ -1,48 +0,0 @@
/**
* @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 {writeFileSync} from 'fs';
import {debug, log} from '../../utils/console';
import {loadCommitMessageDraft} from './commit-message-draft';
import {CommitMsgSource} from './commit-message-source';
/**
* Restore the commit message draft to the git to be used as the default commit message.
*
* The source provided may be one of the sources described in
* https://git-scm.com/docs/githooks#_prepare_commit_msg
*/
export function restoreCommitMessage(filePath: string, source?: CommitMsgSource) {
if (!!source) {
if (source === 'message') {
debug('A commit message was already provided via the command with a -m or -F flag');
}
if (source === 'template') {
debug('A commit message was already provided via the -t flag or config.template setting');
}
if (source === 'squash') {
debug('A commit message was already provided as a merge action or via .git/MERGE_MSG');
}
if (source === 'commit') {
debug('A commit message was already provided through a revision specified via --fixup, -c,');
debug('-C or --amend flag');
}
process.exit(0);
}
/** A draft of a commit message. */
const commitMessage = loadCommitMessageDraft(filePath);
// If the commit message draft has content, restore it into the provided filepath.
if (commitMessage) {
writeFileSync(filePath, commitMessage);
}
// Exit the process
process.exit(0);
}
-30
View File
@@ -1,30 +0,0 @@
/**
* @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
*/
/** The parts that make up a commit message for creating a commit message string. */
export interface CommitMessageParts {
prefix: string;
type: string;
npmScope: string;
scope: string;
summary: string;
body: string;
footer: string;
}
/**
* Generate a commit message builder function, using the provided defaults.
*/
export function commitMessageBuilder(defaults: CommitMessageParts) {
return (params: Partial<CommitMessageParts> = {}) => {
const {prefix, type, npmScope, scope, summary, body, footer} = {...defaults, ...params};
const scopeSlug = npmScope ? `${npmScope}/${scope}` : scope;
return `${prefix}${type}${scopeSlug ? '(' + scopeSlug + ')' : ''}: ${summary}\n\n${body}\n\n${
footer}`;
};
}
-32
View File
@@ -1,32 +0,0 @@
/**
* @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 gitCommits_ from 'git-raw-commits';
import {CommitFromGitLog, gitLogFormatForParsing, parseCommitFromGitLog} from './parse';
// Set `gitCommits` as this imported value to address "Cannot call a namespace" error.
const gitCommits = gitCommits_;
/**
* Find all commits within the given range and return an object describing those.
*/
export function getCommitsInRange(from: string, to: string = 'HEAD'): Promise<CommitFromGitLog[]> {
return new Promise((resolve, reject) => {
/** List of parsed commit objects. */
const commits: CommitFromGitLog[] = [];
/** Stream of raw git commit strings in the range provided. */
const commitStream = gitCommits({from, to, format: gitLogFormatForParsing});
// Accumulate the parsed commits for each commit from the Readable stream into an array, then
// resolve the promise with the array when the Readable stream ends.
commitStream.on('data', (commit: Buffer) => commits.push(parseCommitFromGitLog(commit)));
commitStream.on('error', (err: Error) => reject(err));
commitStream.on('end', () => resolve(commits));
});
}
@@ -1,65 +0,0 @@
/**
* @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 {getUserConfig} from '../../utils/config';
import {validateFile} from './validate-file';
export interface ValidateFileOptions {
file?: string;
fileEnvVariable?: string;
error: boolean;
}
/** Builds the command. */
function builder(yargs: Argv) {
return yargs
.option('file', {
type: 'string',
conflicts: ['file-env-variable'],
description: 'The path of the commit message file.',
})
.option('file-env-variable' as 'fileEnvVariable', {
type: 'string',
conflicts: ['file'],
description: 'The key of the environment variable for the path of the commit message file.',
coerce: (arg: string|undefined) => {
if (arg === undefined) {
return arg;
}
const file = process.env[arg];
if (!file) {
throw new Error(`Provided environment variable "${arg}" was not found.`);
}
return file;
},
})
.option('error', {
type: 'boolean',
description:
'Whether invalid commit messages should be treated as failures rather than a warning',
default: !!getUserConfig().commitMessage?.errorOnInvalidMessage || !!process.env['CI']
});
}
/** Handles the command. */
async function handler({error, file, fileEnvVariable}: Arguments<ValidateFileOptions>) {
const filePath = file || fileEnvVariable || '.git/COMMIT_EDITMSG';
validateFile(filePath, error);
}
/** yargs command module describing the command. */
export const ValidateFileModule: CommandModule<{}, ValidateFileOptions> = {
handler,
builder,
command: 'pre-commit-validate',
describe: 'Validate the most recent commit message',
};
@@ -1,48 +0,0 @@
/**
* @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 {readFileSync} from 'fs';
import {resolve} from 'path';
import {error, green, info, log, red, yellow} from '../../utils/console';
import {GitClient} from '../../utils/git/git-client';
import {deleteCommitMessageDraft, saveCommitMessageDraft} from '../restore-commit-message/commit-message-draft';
import {printValidationErrors, validateCommitMessage} from '../validate';
/** Validate commit message at the provided file path. */
export function validateFile(filePath: string, isErrorMode: boolean) {
const git = GitClient.get();
const commitMessage = readFileSync(resolve(git.baseDir, filePath), 'utf8');
const {valid, errors} = validateCommitMessage(commitMessage);
if (valid) {
info(`${green('√')} Valid commit message`);
deleteCommitMessageDraft(filePath);
process.exitCode = 0;
return;
}
/** Function used to print to the console log. */
let printFn = isErrorMode ? error : log;
printFn(`${isErrorMode ? red('✘') : yellow('!')} Invalid commit message`);
printValidationErrors(errors, printFn);
if (isErrorMode) {
printFn(red('Aborting commit attempt due to invalid commit message.'));
printFn(
red('Commit message aborted as failure rather than warning due to local configuration.'));
} else {
printFn(yellow('Before this commit can be merged into the upstream repository, it must be'));
printFn(yellow('amended to follow commit message guidelines.'));
}
// On all invalid commit messages, the commit message should be saved as a draft to be
// restored on the next commit attempt.
saveCommitMessageDraft(filePath, commitMessage);
// Set the correct exit code based on if invalid commit message is an error.
process.exitCode = isErrorMode ? 1 : 0;
}
@@ -1,56 +0,0 @@
/**
* @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 {info} from '../../utils/console';
import {validateCommitRange} from './validate-range';
export interface ValidateRangeOptions {
startingRef: string;
endingRef: string;
}
/** Builds the command. */
function builder(yargs: Argv) {
return yargs
.positional('startingRef', {
description: 'The first ref in the range to select',
type: 'string',
demandOption: true,
})
.positional('endingRef', {
description: 'The last ref in the range to select',
type: 'string',
default: 'HEAD',
});
}
/** Handles the command. */
async function handler({startingRef, endingRef}: Arguments<ValidateRangeOptions>) {
// If on CI, and no pull request number is provided, assume the branch
// being run on is an upstream branch.
if (process.env['CI'] && process.env['CI_PULL_REQUEST'] === 'false') {
info(`Since valid commit messages are enforced by PR linting on CI, we do not`);
info(`need to validate commit messages on CI runs on upstream branches.`);
info();
info(`Skipping check of provided commit range`);
return;
}
await validateCommitRange(startingRef, endingRef);
}
/** yargs command module describing the command. */
export const ValidateRangeModule: CommandModule<{}, ValidateRangeOptions> = {
handler,
builder,
command: 'validate-range <starting-ref> [ending-ref]',
describe: 'Validate a range of commit messages',
};
@@ -1,59 +0,0 @@
/**
* @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 {error, green, info, red} from '../../utils/console';
import {Commit} from '../parse';
import {getCommitsInRange} from '../utils';
import {printValidationErrors, validateCommitMessage, ValidateCommitMessageOptions} from '../validate';
// Whether the provided commit is a fixup commit.
const isNonFixup = (commit: Commit) => !commit.isFixup;
// Extracts commit header (first line of commit message).
const extractCommitHeader = (commit: Commit) => commit.header;
/** Validate all commits in a provided git commit range. */
export async function validateCommitRange(from: string, to: string) {
/** A list of tuples of the commit header string and a list of error messages for the commit. */
const errors: [commitHeader: string, errors: string[]][] = [];
/** A list of parsed commit messages from the range. */
const commits = await getCommitsInRange(from, to);
info(`Examining ${commits.length} commit(s) in the provided range: ${from}..${to}`);
/**
* Whether all commits in the range are valid, commits are allowed to be fixup commits for other
* commits in the provided commit range.
*/
const allCommitsInRangeValid = commits.every((commit, i) => {
const options: ValidateCommitMessageOptions = {
disallowSquash: true,
nonFixupCommitHeaders: isNonFixup(commit) ?
undefined :
commits.slice(i + 1).filter(isNonFixup).map(extractCommitHeader)
};
const {valid, errors: localErrors} = validateCommitMessage(commit, options);
if (localErrors.length) {
errors.push([commit.header, localErrors]);
}
return valid;
});
if (allCommitsInRangeValid) {
info(green('√ All commit messages in range valid.'));
} else {
error(red('✘ Invalid commit message'));
errors.forEach(([header, validationErrors]) => {
error.group(header);
printValidationErrors(validationErrors);
error.groupEnd();
});
// Exit with a non-zero exit code if invalid commit messages have
// been discovered.
process.exit(1);
}
}
-469
View File
@@ -1,469 +0,0 @@
/**
* @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
*/
// Imports
import * as validateConfig from './config';
import {parseCommitMessage} from './parse';
import {validateCommitMessage, ValidateCommitMessageResult} from './validate';
type CommitMessageConfig = validateConfig.CommitMessageConfig;
// Constants
const config: {commitMessage: CommitMessageConfig} = {
commitMessage: {
maxLineLength: 120,
minBodyLength: 0,
scopes: [
'common',
'compiler',
'core',
'packaging',
'@angular-devkit/build-angular',
]
}
};
const TYPES = Object.keys(validateConfig.COMMIT_TYPES).join(', ');
const SCOPES = config.commitMessage.scopes.join(', ');
const INVALID = false;
const VALID = true;
function expectValidationResult(
validationResult: ValidateCommitMessageResult, valid: boolean, errors: string[] = []) {
expect(validationResult).toEqual(jasmine.objectContaining({valid, errors}));
}
// TODO(josephperrott): Clean up tests to test script rather than for
// specific commit messages we want to use.
describe('validate-commit-message.js', () => {
beforeEach(() => {
spyOn(validateConfig, 'getCommitMessageConfig')
.and.returnValue(config as ReturnType<typeof validateConfig.getCommitMessageConfig>);
});
describe('validateMessage()', () => {
it('should be valid', () => {
expectValidationResult(validateCommitMessage('feat(packaging): something'), VALID);
expectValidationResult(validateCommitMessage('fix(packaging): something'), VALID);
expectValidationResult(validateCommitMessage('fixup! fix(packaging): something'), VALID);
expectValidationResult(validateCommitMessage('squash! fix(packaging): something'), VALID);
expectValidationResult(validateCommitMessage('Revert: "fix(packaging): something"'), VALID);
});
it('should validate max length', () => {
const msg =
'fix(compiler): something super mega extra giga tera long, maybe even longer and longer and longer and longer and longer and longer...';
expectValidationResult(validateCommitMessage(msg), INVALID, [
`The commit message header is longer than ${config.commitMessage.maxLineLength} characters`
]);
});
it('should skip max length limit for URLs', () => {
const msg = 'fix(compiler): this is just a usual commit message title\n\n' +
'This is a normal commit message body which does not exceed the max length\n' +
'limit. For more details see the following super long URL:\n\n' +
'https://github.com/angular/components/commit/e2ace018ddfad10608e0e32932c43dcfef4095d7#diff-9879d6db96fd29134fc802214163b95a';
expectValidationResult(validateCommitMessage(msg), VALID);
});
it('should validate "<type>(<scope>): <subject>" format', () => {
const msg = 'not correct format';
expectValidationResult(
validateCommitMessage(msg), INVALID,
[`The commit message header does not match the expected format.`]);
});
it('should fail when type is invalid', () => {
const msg = 'weird(core): something';
expectValidationResult(
validateCommitMessage(msg), INVALID,
[`'weird' is not an allowed type.\n => TYPES: ${TYPES}`]);
});
it('should pass when scope contains NPM scope', () => {
expectValidationResult(
validateCommitMessage('fix(@angular-devkit/build-angular): something'), true);
});
it('should fail when scope is invalid', () => {
const errorMessageFor = (scope: string, header: string) =>
`'${scope}' is not an allowed scope.\n => SCOPES: ${SCOPES}`;
expectValidationResult(
validateCommitMessage('fix(Compiler): something'), INVALID,
[errorMessageFor('Compiler', 'fix(Compiler): something')]);
expectValidationResult(
validateCommitMessage('feat(bah): something'), INVALID,
[errorMessageFor('bah', 'feat(bah): something')]);
expectValidationResult(
validateCommitMessage('fix(webworker): something'), INVALID,
[errorMessageFor('webworker', 'fix(webworker): something')]);
expectValidationResult(
validateCommitMessage('refactor(security): something'), INVALID,
[errorMessageFor('security', 'refactor(security): something')]);
expectValidationResult(
validateCommitMessage('refactor(docs): something'), INVALID,
[errorMessageFor('docs', 'refactor(docs): something')]);
expectValidationResult(
validateCommitMessage('feat(angular): something'), INVALID,
[errorMessageFor('angular', 'feat(angular): something')]);
});
it('should allow empty scope', () => {
expectValidationResult(validateCommitMessage('build: blablabla'), VALID);
});
// We do not want to allow WIP. It is OK to fail the PR build in this case to show that there is
// work still to be done (i.e. fixing the commit message).
it('should not allow "WIP: ..." syntax', () => {
const msg = 'WIP: fix: something';
expectValidationResult(
validateCommitMessage(msg), INVALID,
[`'WIP' is not an allowed type.\n => TYPES: ${TYPES}`]);
});
describe('(revert)', () => {
it('should allow valid "revert: ..." syntaxes', () => {
expectValidationResult(validateCommitMessage('revert: anything'), VALID);
expectValidationResult(validateCommitMessage('Revert: "anything"'), VALID);
expectValidationResult(validateCommitMessage('revert anything'), VALID);
expectValidationResult(validateCommitMessage('rEvErT anything'), VALID);
});
it('should not allow "revert(scope): ..." syntax', () => {
const msg = 'revert(compiler): reduce generated code payload size by 65%';
expectValidationResult(
validateCommitMessage(msg), INVALID,
[`'revert' is not an allowed type.\n => TYPES: ${TYPES}`]);
});
// https://github.com/angular/angular/issues/23479
it('should allow typical Angular messages generated by git', () => {
const msg =
'Revert "fix(compiler): Pretty print object instead of [Object object] (#22689)" (#23442)';
expectValidationResult(validateCommitMessage(msg), VALID);
});
});
describe('(squash)', () => {
describe('without `disallowSquash`', () => {
it('should return commits as valid', () => {
expectValidationResult(validateCommitMessage('squash! feat(core): add feature'), VALID);
expectValidationResult(validateCommitMessage('squash! fix: a bug'), VALID);
expectValidationResult(validateCommitMessage('squash! fix a typo'), VALID);
});
});
describe('with `disallowSquash`', () => {
it('should fail', () => {
expectValidationResult(
validateCommitMessage('fix(core): something', {disallowSquash: true}), VALID);
expectValidationResult(
validateCommitMessage('squash! fix(core): something', {disallowSquash: true}),
INVALID, ['The commit must be manually squashed into the target commit']);
});
});
});
describe('(fixup)', () => {
describe('without `nonFixupCommitHeaders`', () => {
it('should return commits as valid', () => {
expectValidationResult(validateCommitMessage('fixup! feat(core): add feature'), VALID);
expectValidationResult(validateCommitMessage('fixup! fix: a bug'), VALID);
expectValidationResult(validateCommitMessage('fixup! fixup! fix: a bug'), VALID);
});
});
describe('with `nonFixupCommitHeaders`', () => {
it('should check that the fixup commit matches a non-fixup one', () => {
const msg = 'fixup! foo';
expectValidationResult(
validateCommitMessage(
msg, {disallowSquash: false, nonFixupCommitHeaders: ['foo', 'bar', 'baz']}),
VALID);
expectValidationResult(
validateCommitMessage(
msg, {disallowSquash: false, nonFixupCommitHeaders: ['bar', 'baz', 'foo']}),
VALID);
expectValidationResult(
validateCommitMessage(
msg, {disallowSquash: false, nonFixupCommitHeaders: ['baz', 'foo', 'bar']}),
VALID);
expectValidationResult(
validateCommitMessage(
msg, {disallowSquash: false, nonFixupCommitHeaders: ['qux', 'quux', 'quuux']}),
INVALID,
['Unable to find match for fixup commit among prior commits: \n' +
' qux\n' +
' quux\n' +
' quuux']);
});
it('should fail if `nonFixupCommitHeaders` is empty', () => {
expectValidationResult(
validateCommitMessage(
'refactor(core): make reactive',
{disallowSquash: false, nonFixupCommitHeaders: []}),
VALID);
expectValidationResult(
validateCommitMessage(
'fixup! foo', {disallowSquash: false, nonFixupCommitHeaders: []}),
INVALID, [`Unable to find match for fixup commit among prior commits: -`]);
});
});
});
describe('minBodyLength', () => {
const minBodyLengthConfig: {commitMessage: CommitMessageConfig} = {
commitMessage: {
maxLineLength: 120,
minBodyLength: 30,
minBodyLengthTypeExcludes: ['docs'],
scopes: ['core']
}
};
beforeEach(() => {
(validateConfig.getCommitMessageConfig as jasmine.Spy).and.returnValue(minBodyLengthConfig);
});
it('should fail validation if the body is shorter than `minBodyLength`', () => {
expectValidationResult(
validateCommitMessage(
'fix(core): something\n\n Explanation of the motivation behind this change'),
VALID);
expectValidationResult(
validateCommitMessage('fix(core): something\n\n too short'), INVALID,
['The commit message body does not meet the minimum length of 30 characters']);
expectValidationResult(validateCommitMessage('fix(core): something'), INVALID, [
'The commit message body does not meet the minimum length of 30 characters'
]);
});
it('should pass validation even if the total non-header content is longer than `minBodyLength`, even if the body contains a `#` reference usage',
() => {
expectValidationResult(
validateCommitMessage(
'fix(core): something\n\n Explanation of how #123 motivated this change'),
VALID);
});
it('should pass validation if the body is shorter than `minBodyLength` but the commit type is in the `minBodyLengthTypeExclusions` list',
() => {
expectValidationResult(validateCommitMessage('docs: just fixing a typo'), VALID);
expectValidationResult(validateCommitMessage('docs(core): just fixing a typo'), VALID);
expectValidationResult(
validateCommitMessage(
'docs(core): just fixing a typo\n\nThis was just a silly typo.'),
VALID);
});
});
describe('deprecations', () => {
it('should allow valid deprecation notes in commit messages', () => {
const msgWithListOfDeprecations =
'feat(compiler): this is just a usual commit message title\n\n' +
'This is a normal commit message body which does not exceed the max length\n' +
'limit. For more details see the following super long URL:\n\n' +
'DEPRECATED:\n' +
' * A to be removed\n' +
' * B to be removed';
expectValidationResult(validateCommitMessage(msgWithListOfDeprecations), VALID);
expect(parseCommitMessage(msgWithListOfDeprecations).deprecations.length).toBe(1);
const msgWithSummary = 'feat(compiler): this is just a usual commit message title\n\n' +
'This is a normal commit message body which does not exceed the max length\n' +
'limit. For more details see the following super long URL:\n\n' +
'DEPRECATED: All methods in X to be removed in v12.';
expectValidationResult(validateCommitMessage(msgWithSummary), VALID);
expect(parseCommitMessage(msgWithSummary).deprecations.length).toBe(1);
const msgWithSummaryAndDescription =
'feat(compiler): this is just a usual commit message title\n\n' +
'This is a normal commit message body which does not exceed the max length\n' +
'limit. For more details see the following super long URL:\n\n' +
'DEPRECATED: All methods in X to be removed in v12.\n' +
'' +
'This is the more detailed description about the deprecation of X.';
expectValidationResult(validateCommitMessage(msgWithSummaryAndDescription), VALID);
expect(parseCommitMessage(msgWithSummaryAndDescription).deprecations.length).toBe(1);
const msgWithNoDeprecation =
'feat(compiler): this is just a usual commit message title\n\n' +
'This is not a\n' +
'deprecation commit.';
expectValidationResult(validateCommitMessage(msgWithNoDeprecation), VALID);
expect(parseCommitMessage(msgWithNoDeprecation).deprecations.length).toBe(0);
});
it('should fail for non-valid deprecation notes in commit messages', () => {
const incorrectKeyword1 = 'feat(compiler): this is just a usual commit message title\n\n' +
'This is a normal commit message body which does not exceed the max length\n' +
'limit. For more details see the following super long URL:\n\n' +
'DEPRECATE:\n' +
' * A to be removed\n' +
' * B to be removed';
expectValidationResult(
validateCommitMessage(incorrectKeyword1), INVALID,
['The commit message body contains an invalid deprecation note.']);
const incorrectKeyword2 = 'feat(compiler): this is just a usual commit message title\n\n' +
'This is a normal commit message body which does not exceed the max length\n' +
'limit. For more details see the following super long URL:\n\n' +
'DEPRECATES:\n' +
' * A to be removed\n' +
' * B to be removed';
expectValidationResult(
validateCommitMessage(incorrectKeyword2), INVALID,
['The commit message body contains an invalid deprecation note.']);
const incorrectKeyword3 = 'feat(compiler): this is just a usual commit message title\n\n' +
'This is a normal commit message body which does not exceed the max length\n' +
'limit. For more details see the following super long URL:\n\n' +
'DEPRECATIONS:\n' +
' * A to be removed\n' +
' * B to be removed';
expectValidationResult(
validateCommitMessage(incorrectKeyword3), INVALID,
['The commit message body contains an invalid deprecation note.']);
});
});
describe('breaking change', () => {
it('should allow valid breaking change commit descriptions', () => {
const msgWithSummary = 'feat(compiler): this is just a usual commit message title\n\n' +
'This is a normal commit message body which does not exceed the max length\n' +
'limit. For more details see the following super long URL:\n\n' +
'BREAKING CHANGE: This is a summary of a breaking change.';
expectValidationResult(validateCommitMessage(msgWithSummary), VALID);
expect(parseCommitMessage(msgWithSummary).breakingChanges.length).toBe(1);
const msgWithDescriptionDoubleLineBreakSeparator =
'feat(compiler): this is just a usual commit message title\n\n' +
'This is a normal commit message body which does not exceed the max length\n' +
'limit. For more details see the following super long URL:\n\n' +
'BREAKING CHANGE:\n\n' +
'This is a full description of the breaking change.';
expectValidationResult(
validateCommitMessage(msgWithDescriptionDoubleLineBreakSeparator), VALID);
expect(
parseCommitMessage(msgWithDescriptionDoubleLineBreakSeparator).breakingChanges.length)
.toBe(1);
const msgWithDescriptionSingleLineBreakSeparator =
'feat(compiler): this is just a usual commit message title\n\n' +
'This is a normal commit message body which does not exceed the max length\n' +
'limit. For more details see the following super long URL:\n\n' +
'BREAKING CHANGE:\n' +
'This is a full description of the breaking change.';
expectValidationResult(
validateCommitMessage(msgWithDescriptionSingleLineBreakSeparator), VALID);
expect(
parseCommitMessage(msgWithDescriptionSingleLineBreakSeparator).breakingChanges.length)
.toBe(1);
const msgWithSummaryAndDescription =
'feat(compiler): this is just a usual commit message title\n\n' +
'This is a normal commit message body which does not exceed the max length\n' +
'limit. For more details see the following super long URL:\n\n' +
'BREAKING CHANGE: This is a summary of a breaking change.\n\n' +
'This is a full description of the breaking change.';
expectValidationResult(validateCommitMessage(msgWithSummaryAndDescription), VALID);
expect(parseCommitMessage(msgWithSummaryAndDescription).breakingChanges.length).toBe(1);
const msgWithNonBreaking = 'feat(compiler): this is just a usual commit message title\n\n' +
'This is not a\n' +
'breaking change commit.';
expectValidationResult(validateCommitMessage(msgWithNonBreaking), VALID);
expect(parseCommitMessage(msgWithNonBreaking).breakingChanges.length).toBe(0);
});
it('should fail for non-valid breaking change commit descriptions', () => {
const msgWithSummary = 'feat(compiler): this is just a usual commit message title\n\n' +
'This is a normal commit message body which does not exceed the max length\n' +
'limit. For more details see the following super long URL:\n\n' +
'BREAKING CHANGE This is a summary of a breaking change.';
expectValidationResult(
validateCommitMessage(msgWithSummary), INVALID,
[`The commit message body contains an invalid breaking change note.`]);
const msgWithPlural = 'feat(compiler): this is just a usual commit message title\n\n' +
'This is a normal commit message body which does not exceed the max length\n' +
'limit. For more details see the following super long URL:\n\n' +
'BREAKING CHANGES: This is a summary of a breaking change.';
expectValidationResult(
validateCommitMessage(msgWithPlural), INVALID,
[`The commit message body contains an invalid breaking change note.`]);
const msgWithWithDashedKeyword =
'feat(compiler): this is just a usual commit message title\n\n' +
'This is a normal commit message body which does not exceed the max length\n' +
'limit. For more details see the following super long URL:\n\n' +
'BREAKING-CHANGE:' +
'This is a full description of the breaking change.';
expectValidationResult(
validateCommitMessage(msgWithWithDashedKeyword), INVALID,
[`The commit message body contains an invalid breaking change note.`]);
const msgWithSummaryAndDescription =
'feat(compiler): this is just a usual commit message title\n\n' +
'This is a normal commit message body which does not exceed the max length\n' +
'limit. For more details see the following super long URL:\n\n' +
'BREAKING CHANGE\n\n' +
'This is a full description of the breaking change.';
expectValidationResult(
validateCommitMessage(msgWithSummaryAndDescription), INVALID,
[`The commit message body contains an invalid breaking change note.`]);
const incorrectKeyword1 = 'feat(compiler): this is just a usual commit message title\n\n' +
'This is a normal commit message body which does not exceed the max length\n' +
'limit. For more details see the following super long URL:\n\n' +
'BREAKING CHANGES:\n' +
' * A has been removed\n';
expectValidationResult(
validateCommitMessage(incorrectKeyword1), INVALID,
['The commit message body contains an invalid breaking change note.']);
const incorrectKeyword2 = 'feat(compiler): this is just a usual commit message title\n\n' +
'This is a normal commit message body which does not exceed the max length\n' +
'limit. For more details see the following super long URL:\n\n' +
'BREAKING-CHANGE:\n' +
' * A has been removed\n';
expectValidationResult(
validateCommitMessage(incorrectKeyword2), INVALID,
['The commit message body contains an invalid breaking change note.']);
const incorrectKeyword3 = 'feat(compiler): this is just a usual commit message title\n\n' +
'This is a normal commit message body which does not exceed the max length\n' +
'limit. For more details see the following super long URL:\n\n' +
'BREAKING-CHANGES:\n' +
' * A has been removed\n';
expectValidationResult(
validateCommitMessage(incorrectKeyword3), INVALID,
['The commit message body contains an invalid breaking change note.']);
});
});
});
});
-210
View File
@@ -1,210 +0,0 @@
/**
* @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 {error} from '../utils/console';
import {COMMIT_TYPES, getCommitMessageConfig, ScopeRequirement} from './config';
import {Commit, parseCommitMessage} from './parse';
/** Options for commit message validation. */
export interface ValidateCommitMessageOptions {
disallowSquash?: boolean;
nonFixupCommitHeaders?: string[];
}
/** The result of a commit message validation check. */
export interface ValidateCommitMessageResult {
valid: boolean;
errors: string[];
commit: Commit;
}
/** Regex matching a URL for an entire commit body line. */
const COMMIT_BODY_URL_LINE_RE = /^https?:\/\/.*$/;
/**
* Regular expression matching potential misuse of the `BREAKING CHANGE:` marker in a
* commit message. Commit messages containing one of the following snippets will fail:
*
* - `BREAKING CHANGE <some-content>` | Here we assume the colon is missing by accident.
* - `BREAKING-CHANGE: <some-content>` | The wrong keyword is used here.
* - `BREAKING CHANGES: <some-content>` | The wrong keyword is used here.
* - `BREAKING-CHANGES: <some-content>` | The wrong keyword is used here.
*/
const INCORRECT_BREAKING_CHANGE_BODY_RE =
/^(BREAKING CHANGE[^:]|BREAKING-CHANGE|BREAKING[ -]CHANGES)/m;
/**
* Regular expression matching potential misuse of the `DEPRECATED:` marker in a commit
* message. Commit messages containing one of the following snippets will fail:
*
* - `DEPRECATED <some-content>` | Here we assume the colon is missing by accident.
* - `DEPRECATIONS: <some-content>` | The wrong keyword is used here.
* - `DEPRECATE: <some-content>` | The wrong keyword is used here.
* - `DEPRECATES: <some-content>` | The wrong keyword is used here.
*/
const INCORRECT_DEPRECATION_BODY_RE = /^(DEPRECATED[^:]|DEPRECATIONS|DEPRECATE:|DEPRECATES)/m;
/** Validate a commit message against using the local repo's config. */
export function validateCommitMessage(
commitMsg: string|Commit,
options: ValidateCommitMessageOptions = {}): ValidateCommitMessageResult {
const config = getCommitMessageConfig().commitMessage;
const commit = typeof commitMsg === 'string' ? parseCommitMessage(commitMsg) : commitMsg;
const errors: string[] = [];
/** Perform the validation checks against the parsed commit. */
function validateCommitAndCollectErrors() {
////////////////////////////////////
// Checking revert, squash, fixup //
////////////////////////////////////
// All revert commits are considered valid.
if (commit.isRevert) {
return true;
}
// All squashes are considered valid, as the commit will be squashed into another in
// the git history anyway, unless the options provided to not allow squash commits.
if (commit.isSquash) {
if (options.disallowSquash) {
errors.push('The commit must be manually squashed into the target commit');
return false;
}
return true;
}
// Fixups commits are considered valid, unless nonFixupCommitHeaders are provided to check
// against. If `nonFixupCommitHeaders` is not empty, we check whether there is a corresponding
// non-fixup commit (i.e. a commit whose header is identical to this commit's header after
// stripping the `fixup! ` prefix), otherwise we assume this verification will happen in another
// check.
if (commit.isFixup) {
if (options.nonFixupCommitHeaders && !options.nonFixupCommitHeaders.includes(commit.header)) {
errors.push(
'Unable to find match for fixup commit among prior commits: ' +
(options.nonFixupCommitHeaders.map(x => `\n ${x}`).join('') || '-'));
return false;
}
return true;
}
////////////////////////////
// Checking commit header //
////////////////////////////
if (commit.header.length > config.maxLineLength) {
errors.push(`The commit message header is longer than ${config.maxLineLength} characters`);
return false;
}
if (!commit.type) {
errors.push(`The commit message header does not match the expected format.`);
return false;
}
if (COMMIT_TYPES[commit.type] === undefined) {
errors.push(`'${commit.type}' is not an allowed type.\n => TYPES: ${
Object.keys(COMMIT_TYPES).join(', ')}`);
return false;
}
/** The scope requirement level for the provided type of the commit message. */
const scopeRequirementForType = COMMIT_TYPES[commit.type].scope;
if (scopeRequirementForType === ScopeRequirement.Forbidden && commit.scope) {
errors.push(`Scopes are forbidden for commits with type '${commit.type}', but a scope of '${
commit.scope}' was provided.`);
return false;
}
if (scopeRequirementForType === ScopeRequirement.Required && !commit.scope) {
errors.push(
`Scopes are required for commits with type '${commit.type}', but no scope was provided.`);
return false;
}
const fullScope = commit.npmScope ? `${commit.npmScope}/${commit.scope}` : commit.scope;
if (fullScope && !config.scopes.includes(fullScope)) {
errors.push(
`'${fullScope}' is not an allowed scope.\n => SCOPES: ${config.scopes.join(', ')}`);
return false;
}
// Commits with the type of `release` do not require a commit body.
if (commit.type === 'release') {
return true;
}
//////////////////////////
// Checking commit body //
//////////////////////////
// Due to an issue in which conventional-commits-parser considers all parts of a commit after
// a `#` reference to be the footer, we check the length of all of the commit content after the
// header. In the future, we expect to be able to check only the body once the parser properly
// handles this case.
const allNonHeaderContent = `${commit.body.trim()}\n${commit.footer.trim()}`;
if (!config.minBodyLengthTypeExcludes?.includes(commit.type) &&
allNonHeaderContent.length < config.minBodyLength) {
errors.push(`The commit message body does not meet the minimum length of ${
config.minBodyLength} characters`);
return false;
}
const bodyByLine = commit.body.split('\n');
const lineExceedsMaxLength = bodyByLine.some((line: string) => {
// Check if any line exceeds the max line length limit. The limit is ignored for
// lines that just contain an URL (as these usually cannot be wrapped or shortened).
return line.length > config.maxLineLength && !COMMIT_BODY_URL_LINE_RE.test(line);
});
if (lineExceedsMaxLength) {
errors.push(`The commit message body contains lines greater than ${
config.maxLineLength} characters.`);
return false;
}
// Breaking change
// Check if the commit message contains a valid break change description.
// https://github.com/angular/angular/blob/88fbc066775ab1a2f6a8c75f933375b46d8fa9a4/CONTRIBUTING.md#commit-message-footer
if (INCORRECT_BREAKING_CHANGE_BODY_RE.test(commit.fullText)) {
errors.push(`The commit message body contains an invalid breaking change note.`);
return false;
}
if (INCORRECT_DEPRECATION_BODY_RE.test(commit.fullText)) {
errors.push(`The commit message body contains an invalid deprecation note.`);
return false;
}
return true;
}
return {valid: validateCommitAndCollectErrors(), errors, commit};
}
/** Print the error messages from the commit message validation to the console. */
export function printValidationErrors(errors: string[], print = error) {
print.group(`Error${errors.length === 1 ? '' : 's'}:`);
errors.forEach(line => print(line));
print.groupEnd();
print();
print('The expected format for a commit is: ');
print('<type>(<scope>): <summary>');
print();
print('<body>');
print();
print(`BREAKING CHANGE: <breaking change summary>`);
print();
print(`<breaking change description>`);
print();
print();
}
-67
View File
@@ -1,67 +0,0 @@
"""
Defaults for the `//dev-infra` Bazel package. These are different than
the defaults in `//tools:defaults.bzl` which are specific to the package
structure as seen within `/packages/`.
"""
load("@build_bazel_rules_nodejs//:index.bzl", "generated_file_test")
load("@npm//@bazel/jasmine:index.bzl", _jasmine_node_test = "jasmine_node_test")
load("@npm//@bazel/typescript:index.bzl", _ts_library = "ts_library")
load("@npm//@bazel/rollup:index.bzl", "rollup_bundle")
NPM_PACKAGE_NAME = "@angular/dev-infra-private"
def _compute_module_name():
current_pkg = native.package_name()
if current_pkg == "dev-infra":
return NPM_PACKAGE_NAME
# For deep targets within `//dev-infra` construct the module name in a way that matches
# the structure within the NPM package (i.e. simply appending the actual package path)
return "%s/%s" % (NPM_PACKAGE_NAME, current_pkg[len("dev-infra/"):])
def ts_library(name, **kwargs):
_ts_library(
name = name,
# If no `module_name` is set, compute a module name based on the current Bazel
# package. The module names should match the NPM package structure so that the NPM
# package can be used properly. Note that we disallow any custom `module_name` for
# `//dev-infra` as this usually signifies a mistake we want to raise awareness for.
module_name = _compute_module_name(),
# We use the module name as package name, so that the target can be resolved within
# NodeJS executions, by activating the Bazel NodeJS linker.
# See: https://github.com/bazelbuild/rules_nodejs/pull/2799.
package_name = _compute_module_name(),
**kwargs
)
def jasmine_node_test(**kwargs):
_jasmine_node_test(**kwargs)
# This file continues to serve as indicator for `rules_nodejs` and instructs it to preserve the
# content output in the NPM install workspace. This allows consumers to use rules and targets from
# within Bazel. e.g. by using `@npm//@angular/dev-infra-private/<..>`.
# See: https://github.com/bazelbuild/rules_nodejs/commit/4f508b1a0be1f5444e9c13b0439e649449792fef.
def ng_dev_rolled_up_generated_file(name, entry_point, deps = [], rollup_args = []):
"""Rollup and generated file test macro.
This provides a single macro to create a rollup bundled script and a generated file
test for the created script to ensure it stays up to date in the repository.
"""
rollup_bundle(
name = "%s_bundle" % name,
args = rollup_args,
entry_point = entry_point,
format = "cjs",
silent = True,
sourcemap = "false",
deps = deps,
)
generated_file_test(
name = name,
src = "%s.js" % name,
generated = "%s_bundle" % name,
)
-18
View File
@@ -1,18 +0,0 @@
load("//dev-infra:defaults.bzl", "ts_library")
ts_library(
name = "format",
srcs = glob([
"**/*.ts",
]),
visibility = ["//dev-infra:__subpackages__"],
deps = [
"//dev-infra/utils",
"@npm//@types/cli-progress",
"@npm//@types/node",
"@npm//@types/yargs",
"@npm//cli-progress",
"@npm//multimatch",
"@npm//yargs",
],
)
-52
View File
@@ -1,52 +0,0 @@
/**
* @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 {GitClient} from '../utils/git/git-client';
import {checkFiles, formatFiles} from './format';
/** Build the parser for the format commands. */
export function buildFormatParser(localYargs: yargs.Argv) {
return localYargs.help()
.strict()
.demandCommand()
.option('check', {
type: 'boolean',
default: process.env['CI'] ? true : false,
description: 'Run the formatter to check formatting rather than updating code format'
})
.command(
'all', 'Run the formatter on all files in the repository', args => args,
({check}) => {
const executionCmd = check ? checkFiles : formatFiles;
const allFiles = GitClient.get().allFiles();
executionCmd(allFiles);
})
.command(
'changed [shaOrRef]', 'Run the formatter on files changed since the provided sha/ref',
args => args.positional('shaOrRef', {type: 'string'}),
({shaOrRef, check}) => {
const sha = shaOrRef || 'master';
const executionCmd = check ? checkFiles : formatFiles;
const allChangedFilesSince = GitClient.get().allChangesFilesSince(sha);
executionCmd(allChangedFilesSince);
})
.command(
'staged', 'Run the formatter on all staged files', args => args,
({check}) => {
const executionCmd = check ? checkFiles : formatFiles;
const allStagedFiles = GitClient.get().allStagedFiles();
executionCmd(allStagedFiles);
})
.command(
'files <files..>', 'Run the formatter on provided files',
args => args.positional('files', {array: true, type: 'string'}), ({check, files}) => {
const executionCmd = check ? checkFiles : formatFiles;
executionCmd(files!);
});
}
-51
View File
@@ -1,51 +0,0 @@
/**
* @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 {assertNoErrors, getConfig, NgDevConfig} from '../utils/config';
interface Formatter {
matchers: string[];
}
export interface FormatConfig {
[key: string]: boolean|Formatter;
}
/** Retrieve and validate the config as `FormatConfig`. */
export function getFormatConfig() {
// List of errors encountered validating the config.
const errors: string[] = [];
// The unvalidated config object.
const config: Partial<NgDevConfig<{format: FormatConfig}>> = getConfig();
if (config.format === undefined) {
errors.push(`No configuration defined for "format"`);
}
for (const [key, value] of Object.entries(config.format!)) {
switch (typeof value) {
case 'boolean':
break;
case 'object':
checkFormatterConfig(key, value, errors);
break;
default:
errors.push(`"format.${key}" is not a boolean or Formatter object`);
}
}
assertNoErrors(errors);
return config as Required<typeof config>;
}
/** Validate an individual Formatter config. */
function checkFormatterConfig(key: string, config: Partial<Formatter>, errors: string[]) {
if (config.matchers === undefined) {
errors.push(`Missing "format.${key}.matchers" value`);
}
}
-80
View File
@@ -1,80 +0,0 @@
/**
* @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 {error, info, promptConfirm, red} from '../utils/console';
import {runFormatterInParallel} from './run-commands-parallel';
/**
* Format provided files in place.
*/
export async function formatFiles(files: string[]) {
// Whether any files failed to format.
let failures = await runFormatterInParallel(files, 'format');
if (failures === false) {
info('No files matched for formatting.');
process.exit(0);
}
// The process should exit as a failure if any of the files failed to format.
if (failures.length !== 0) {
error(red(`The following files could not be formatted:`));
failures.forEach(({filePath, message}) => {
info(` • ${filePath}: ${message}`);
});
error(red(`Formatting failed, see errors above for more information.`));
process.exit(1);
}
info(`√ Formatting complete.`);
process.exit(0);
}
/**
* Check provided files for formatting correctness.
*/
export async function checkFiles(files: string[]) {
// Files which are currently not formatted correctly.
const failures = await runFormatterInParallel(files, 'check');
if (failures === false) {
info('No files matched for formatting check.');
process.exit(0);
}
if (failures.length) {
// Provide output expressing which files are failing formatting.
info.group('\nThe following files are out of format:');
for (const {filePath} of failures) {
info(` • ${filePath}`);
}
info.groupEnd();
info();
// If the command is run in a non-CI environment, prompt to format the files immediately.
let runFormatter = false;
if (!process.env['CI']) {
runFormatter = await promptConfirm('Format the files now?', true);
}
if (runFormatter) {
// Format the failing files as requested.
await formatFiles(failures.map(f => f.filePath));
process.exit(0);
} else {
// Inform user how to format files in the future.
info();
info(`To format the failing file run the following command:`);
info(` yarn ng-dev format files ${failures.map(f => f.filePath).join(' ')}`);
process.exit(1);
}
} else {
info('√ All files correctly formatted.');
process.exit(0);
}
}
@@ -1,102 +0,0 @@
/**
* @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 {GitClient} from '../../utils/git/git-client';
import {FormatConfig} from '../config';
// A callback to determine if the formatter run found a failure in formatting.
export type CallbackFunc =
(file: string, code: number|NodeJS.Signals, stdout: string, stderr: string) => boolean;
// The actions a formatter can take.
export type FormatterAction = 'check'|'format';
// The metadata needed for running one of the `FormatterAction`s on a file.
interface FormatterActionMetadata {
commandFlags: string;
callback: CallbackFunc;
}
/**
* The base class for formatters to run against provided files.
*/
export abstract class Formatter {
protected git = GitClient.get();
/**
* The name of the formatter, this is used for identification in logging and for enabling and
* configuring the formatter in the config.
*/
abstract name: string;
/** The full path file location of the formatter binary. */
abstract binaryFilePath: string;
/** Metadata for each `FormatterAction` available to the formatter. */
abstract actions: {
// An action performing a check of format without making any changes.
check: FormatterActionMetadata;
// An action to format files in place.
format: FormatterActionMetadata;
};
/** The default matchers for the formatter for filtering files to be formatted. */
abstract defaultFileMatcher: string[];
constructor(protected config: FormatConfig) {}
/**
* Retrieve the command to execute the provided action, including both the binary
* and command line flags.
*/
commandFor(action: FormatterAction) {
switch (action) {
case 'check':
return `${this.binaryFilePath} ${this.actions.check.commandFlags}`;
case 'format':
return `${this.binaryFilePath} ${this.actions.format.commandFlags}`;
default:
throw Error('Unknown action type');
}
}
/**
* Retrieve the callback for the provided action to determine if an action
* failed in formatting.
*/
callbackFor(action: FormatterAction) {
switch (action) {
case 'check':
return this.actions.check.callback;
case 'format':
return this.actions.format.callback;
default:
throw Error('Unknown action type');
}
}
/** Whether the formatter is enabled in the provided config. */
isEnabled() {
return !!this.config[this.name];
}
/** Retrieve the active file matcher for the formatter. */
getFileMatcher() {
return this.getFileMatcherFromConfig() || this.defaultFileMatcher;
}
/**
* Retrieves the file matcher from the config provided to the constructor if provided.
*/
private getFileMatcherFromConfig(): string[]|undefined {
const formatterConfig = this.config[this.name];
if (typeof formatterConfig === 'boolean') {
return undefined;
}
return formatterConfig.matchers;
}
}
-54
View File
@@ -1,54 +0,0 @@
/**
* @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 {join} from 'path';
import {error} from '../../utils/console';
import {Formatter} from './base-formatter';
/**
* Formatter for running buildifier against bazel related files.
*/
export class Buildifier extends Formatter {
override name = 'buildifier';
override binaryFilePath = join(this.git.baseDir, 'node_modules/.bin/buildifier');
override defaultFileMatcher = ['**/*.bzl', '**/BUILD.bazel', '**/WORKSPACE', '**/BUILD'];
override actions = {
check: {
commandFlags: `${BAZEL_WARNING_FLAG} --lint=warn --mode=check --format=json`,
callback:
(_: string, code: number|NodeJS.Signals, stdout: string) => {
return code !== 0 || !(JSON.parse(stdout) as {success: string}).success;
},
},
format: {
commandFlags: `${BAZEL_WARNING_FLAG} --lint=fix --mode=fix`,
callback:
(file: string, code: number|NodeJS.Signals, _: string, stderr: string) => {
if (code !== 0) {
error(`Error running buildifier on: ${file}`);
error(stderr);
error();
return true;
}
return false;
}
}
};
}
// The warning flag for buildifier copied from angular/angular's usage.
const BAZEL_WARNING_FLAG = `--warnings=attr-cfg,attr-license,attr-non-empty,attr-output-default,` +
`attr-single-file,constant-glob,ctx-args,depset-iteration,depset-union,dict-concatenation,` +
`duplicated-name,filetype,git-repository,http-archive,integer-division,load,load-on-top,` +
`native-build,native-package,output-group,package-name,package-on-top,positional-args,` +
`redefined-variable,repository-name,same-origin-load,string-iteration,unused-variable`;

Some files were not shown because too many files have changed in this diff Show More