mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
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:
@@ -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
|
||||
|
||||
@@ -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
@@ -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,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,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
@@ -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
@@ -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
@@ -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
@@ -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 = {
|
||||
|
||||
@@ -1155,7 +1155,6 @@ groups:
|
||||
'.ng-dev/**',
|
||||
'.vscode/**',
|
||||
'.yarn/**',
|
||||
'dev-infra/**',
|
||||
'docs/*.md',
|
||||
'docs/images/**',
|
||||
'goldens/*',
|
||||
|
||||
+2
-3
@@ -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
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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",
|
||||
],
|
||||
)
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
@@ -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`);
|
||||
}
|
||||
@@ -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",
|
||||
],
|
||||
)
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
@@ -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(["*"]),
|
||||
)
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
@@ -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",
|
||||
],
|
||||
)
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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>;
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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}
|
||||
};
|
||||
}
|
||||
@@ -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';
|
||||
}
|
||||
}
|
||||
@@ -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',
|
||||
};
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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};
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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)
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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>;
|
||||
}
|
||||
@@ -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}`);
|
||||
}
|
||||
@@ -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 +0,0 @@
|
||||
pr/merge/strategies/commit-message-filter.js
|
||||
@@ -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",
|
||||
],
|
||||
)
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
};
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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}`;
|
||||
};
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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.']);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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,
|
||||
)
|
||||
@@ -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",
|
||||
],
|
||||
)
|
||||
@@ -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!);
|
||||
});
|
||||
}
|
||||
@@ -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`);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
Reference in New Issue
Block a user