From 81e42577612298e7e8399c833d1d07c7c402b7ce Mon Sep 17 00:00:00 2001 From: Joey Perrott Date: Thu, 5 Aug 2021 16:56:46 -0700 Subject: [PATCH] build: remove dev-infra directory and migrate to relying on @angular/dev-infra-private-builds (#43061) Remove the dev-infra directory and complete the migration to using the code generated by the angular/dev-infra repository. PR Close #43061 --- .bazelrc | 10 +- .github/angular-robot.yml | 1 - .gitmessage | 3 +- .ng-dev/caretaker.ts | 2 +- .ng-dev/commit-message.ts | 2 +- .ng-dev/format.ts | 2 +- .ng-dev/github.ts | 2 +- .ng-dev/merge.ts | 4 +- .ng-dev/release.ts | 2 +- .pullapprove.yml | 1 - CONTRIBUTING.md | 5 +- WORKSPACE | 2 +- dev-infra/BUILD.bazel | 116 - dev-infra/bazel/BUILD.bazel | 13 - dev-infra/bazel/api-golden/BUILD.bazel | 35 - .../bazel/api-golden/find_entry_points.ts | 62 - dev-infra/bazel/api-golden/index.bzl | 127 - dev-infra/bazel/api-golden/index.ts | 47 - .../bazel/api-golden/index_npm_packages.ts | 74 - dev-infra/bazel/api-golden/test_api_report.ts | 142 - dev-infra/bazel/browsers/BUILD.bazel | 10 - dev-infra/bazel/browsers/README.md | 110 - .../bazel/browsers/browser_archive_repo.bzl | 96 - .../bazel/browsers/browser_configure.bzl | 69 - .../bazel/browsers/browser_repositories.bzl | 15 - dev-infra/bazel/browsers/chromium/BUILD.bazel | 30 - .../bazel/browsers/chromium/chromium.bzl | 76 - .../bazel/browsers/chromium/chromium.json | 20 - dev-infra/bazel/browsers/firefox/BUILD.bazel | 33 - dev-infra/bazel/browsers/firefox/firefox.bzl | 54 - dev-infra/bazel/browsers/firefox/firefox.json | 21 - dev-infra/bazel/expand_template.bzl | 45 - dev-infra/bazel/extract_js_module_output.bzl | 78 - dev-infra/bazel/remote-execution/BUILD.bazel | 36 - .../bazel/remote-execution/cpp/BUILD.bazel | 57 - dev-infra/benchmark/BUILD.bazel | 11 - dev-infra/benchmark/brotli-cli/BUILD.bazel | 19 - dev-infra/benchmark/brotli-cli/cli.js | 21 - .../benchmark/component_benchmark/BUILD.bazel | 12 - .../component_benchmark/benchmark_test.bzl | 27 - .../component_benchmark.bzl | 149 - .../component_benchmark/defaults/BUILD.bazel | 13 - .../component_benchmark/defaults/index.html | 14 - .../component_benchmark/defaults/index.ts | 19 - .../component_benchmark/defaults/styles.css | 7 - .../protractor-perf.conf.js | 43 - .../component_benchmark/start-server.js | 18 - .../component_benchmark/tsconfig-e2e.json | 6 - .../benchmark/driver-utilities/BUILD.bazel | 17 - .../benchmark/driver-utilities/e2e_util.ts | 50 - dev-infra/benchmark/driver-utilities/index.ts | 9 - .../benchmark/driver-utilities/perf_util.ts | 85 - .../benchmark/ng_rollup_bundle/BUILD.bazel | 26 - .../ng_rollup_bundle/ng_rollup_bundle.bzl | 104 - .../ng_rollup_bundle/rollup.config-tmpl.js | 103 - .../ng_rollup_bundle/terser_config.json | 18 - dev-infra/build-worker.js | 609 -- dev-infra/caretaker/BUILD.bazel | 47 - dev-infra/caretaker/check/base.spec.ts | 42 - dev-infra/caretaker/check/base.ts | 27 - dev-infra/caretaker/check/check.ts | 39 - dev-infra/caretaker/check/ci.spec.ts | 117 - dev-infra/caretaker/check/ci.ts | 88 - dev-infra/caretaker/check/cli.ts | 36 - dev-infra/caretaker/check/g3.spec.ts | 109 - dev-infra/caretaker/check/g3.ts | 156 - dev-infra/caretaker/check/github.spec.ts | 119 - dev-infra/caretaker/check/github.ts | 120 - dev-infra/caretaker/check/services.spec.ts | 68 - dev-infra/caretaker/check/services.ts | 81 - dev-infra/caretaker/cli.ts | 17 - dev-infra/caretaker/config.ts | 30 - dev-infra/caretaker/handoff/cli.ts | 36 - .../caretaker/handoff/update-github-team.ts | 132 - dev-infra/cli.ts | 37 - dev-infra/commit-message-filter.js | 1 - dev-infra/commit-message/BUILD.bazel | 43 - dev-infra/commit-message/cli.ts | 21 - dev-infra/commit-message/config.ts | 110 - dev-infra/commit-message/parse.spec.ts | 190 - dev-infra/commit-message/parse.ts | 169 - .../restore-commit-message/cli.ts | 65 - .../commit-message-draft.ts | 30 - .../commit-message-source.ts | 13 - .../restore-commit-message.ts | 48 - dev-infra/commit-message/test-util.ts | 30 - dev-infra/commit-message/utils.ts | 32 - dev-infra/commit-message/validate-file/cli.ts | 65 - .../validate-file/validate-file.ts | 48 - .../commit-message/validate-range/cli.ts | 56 - .../validate-range/validate-range.ts | 59 - dev-infra/commit-message/validate.spec.ts | 469 - dev-infra/commit-message/validate.ts | 210 - dev-infra/defaults.bzl | 67 - dev-infra/format/BUILD.bazel | 18 - dev-infra/format/cli.ts | 52 - dev-infra/format/config.ts | 51 - dev-infra/format/format.ts | 80 - dev-infra/format/formatters/base-formatter.ts | 102 - dev-infra/format/formatters/buildifier.ts | 54 - dev-infra/format/formatters/clang-format.ts | 47 - dev-infra/format/formatters/index.ts | 28 - dev-infra/format/formatters/prettier.ts | 56 - dev-infra/format/run-commands-parallel.ts | 122 - dev-infra/index.bzl | 9 - dev-infra/misc/BUILD.bazel | 15 - dev-infra/misc/build-and-link/cli.ts | 69 - dev-infra/misc/cli.ts | 15 - dev-infra/ng-dev.js | 8340 ----------------- dev-infra/ngbot/BUILD.bazel | 18 - dev-infra/ngbot/cli.ts | 15 - dev-infra/ngbot/verify.ts | 32 - dev-infra/pr/BUILD.bazel | 15 - .../pr/check-target-branches/BUILD.bazel | 12 - .../check-target-branches.ts | 63 - dev-infra/pr/check-target-branches/cli.ts | 37 - dev-infra/pr/checkout/BUILD.bazel | 12 - dev-infra/pr/checkout/cli.ts | 36 - dev-infra/pr/cli.ts | 32 - dev-infra/pr/common/BUILD.bazel | 12 - dev-infra/pr/common/checkout-pr.ts | 128 - .../pr/discover-new-conflicts/BUILD.bazel | 17 - dev-infra/pr/discover-new-conflicts/cli.ts | 54 - dev-infra/pr/discover-new-conflicts/index.ts | 156 - dev-infra/pr/merge/BUILD.bazel | 60 - dev-infra/pr/merge/cli.ts | 50 - dev-infra/pr/merge/config.ts | 132 - dev-infra/pr/merge/defaults/index.ts | 10 - .../pr/merge/defaults/integration.spec.ts | 508 - dev-infra/pr/merge/defaults/labels.ts | 129 - dev-infra/pr/merge/defaults/lts-branch.ts | 68 - dev-infra/pr/merge/failures.ts | 121 - dev-infra/pr/merge/index.ts | 145 - dev-infra/pr/merge/messages.ts | 21 - dev-infra/pr/merge/pull-request.ts | 273 - dev-infra/pr/merge/strategies/api-merge.ts | 234 - .../pr/merge/strategies/autosquash-merge.ts | 113 - .../merge/strategies/commit-message-filter.js | 45 - dev-infra/pr/merge/strategies/strategy.ts | 141 - dev-infra/pr/merge/string-pattern.ts | 12 - dev-infra/pr/merge/target-label.ts | 60 - dev-infra/pr/merge/task.ts | 165 - dev-infra/pr/rebase/BUILD.bazel | 22 - dev-infra/pr/rebase/cli.ts | 30 - dev-infra/pr/rebase/index.ts | 154 - dev-infra/pullapprove/BUILD.bazel | 47 - dev-infra/pullapprove/cli.ts | 15 - dev-infra/pullapprove/condition_evaluator.ts | 64 - dev-infra/pullapprove/group.ts | 132 - dev-infra/pullapprove/logging.ts | 46 - dev-infra/pullapprove/parse-yaml.ts | 45 - dev-infra/pullapprove/pullapprove_arrays.ts | 89 - dev-infra/pullapprove/utils.ts | 24 - dev-infra/pullapprove/verify.spec.ts | 88 - dev-infra/pullapprove/verify.ts | 114 - dev-infra/release/BUILD.bazel | 22 - dev-infra/release/build/BUILD.bazel | 40 - dev-infra/release/build/build-worker.ts | 32 - dev-infra/release/build/build.spec.ts | 77 - dev-infra/release/build/cli.ts | 75 - dev-infra/release/build/index.ts | 37 - dev-infra/release/cli.ts | 28 - dev-infra/release/config/BUILD.bazel | 13 - dev-infra/release/config/index.ts | 72 - dev-infra/release/info/BUILD.bazel | 14 - dev-infra/release/info/cli.ts | 32 - dev-infra/release/notes/BUILD.bazel | 21 - dev-infra/release/notes/cli.ts | 84 - dev-infra/release/notes/context.ts | 178 - dev-infra/release/notes/release-notes.ts | 92 - .../release/notes/templates/changelog.ts | 92 - .../release/notes/templates/github-release.ts | 87 - dev-infra/release/publish/BUILD.bazel | 28 - dev-infra/release/publish/actions-error.ts | 29 - dev-infra/release/publish/actions.ts | 543 -- .../publish/actions/branch-off-next-branch.ts | 120 - .../actions/configure-next-as-major.ts | 53 - .../release/publish/actions/cut-lts-patch.ts | 83 - .../release/publish/actions/cut-new-patch.ts | 44 - .../publish/actions/cut-next-prerelease.ts | 73 - ...ut-release-candidate-for-feature-freeze.ts | 43 - .../release/publish/actions/cut-stable.ts | 83 - dev-infra/release/publish/actions/index.ts | 33 - .../actions/move-next-into-feature-freeze.ts | 28 - .../move-next-into-release-candidate.ts | 27 - .../actions/tag-recent-major-as-latest.ts | 53 - dev-infra/release/publish/cli.ts | 59 - dev-infra/release/publish/commit-message.ts | 39 - dev-infra/release/publish/constants.ts | 16 - .../release/publish/external-commands.ts | 92 - dev-infra/release/publish/graphql-queries.ts | 31 - dev-infra/release/publish/index.ts | 214 - .../release/publish/pull-request-state.ts | 79 - dev-infra/release/publish/test/BUILD.bazel | 36 - .../test/branch-off-next-branch-testing.ts | 103 - dev-infra/release/publish/test/common.spec.ts | 163 - .../test/configure-next-as-major.spec.ts | 79 - .../publish/test/cut-lts-patch.spec.ts | 110 - .../publish/test/cut-new-patch.spec.ts | 52 - .../publish/test/cut-next-prerelease.spec.ts | 79 - ...lease-candidate-for-feature-freeze.spec.ts | 49 - .../release/publish/test/cut-stable.spec.ts | 87 - .../publish/test/github-api-testing.ts | 81 - .../move-next-into-feature-freeze.spec.ts | 68 - .../move-next-into-release-candidate.spec.ts | 58 - .../test/release-notes/context.spec.ts | 168 - .../test/release-notes/release-notes-utils.ts | 43 - .../test/tag-recent-major-as-latest.spec.ts | 116 - dev-infra/release/publish/test/test-utils.ts | 245 - dev-infra/release/set-dist-tag/BUILD.bazel | 42 - dev-infra/release/set-dist-tag/cli.ts | 78 - .../release/set-dist-tag/set-dist-tag.spec.ts | 73 - dev-infra/release/stamping/cli.ts | 36 - dev-infra/release/stamping/env-stamp.ts | 112 - dev-infra/release/versioning/BUILD.bazel | 17 - dev-infra/release/versioning/README.md | 5 - .../versioning/active-release-trains.ts | 139 - dev-infra/release/versioning/index.ts | 13 - .../release/versioning/long-term-support.ts | 107 - .../versioning/next-prerelease-version.ts | 32 - dev-infra/release/versioning/npm-publish.ts | 95 - dev-infra/release/versioning/npm-registry.ts | 69 - .../release/versioning/print-active-trains.ts | 80 - .../release/versioning/release-trains.ts | 21 - .../release/versioning/version-branches.ts | 94 - dev-infra/tmpl-package.json | 63 - .../ts-circular-dependencies/BUILD.bazel | 15 - dev-infra/ts-circular-dependencies/README.md | 82 - .../ts-circular-dependencies/analyzer.ts | 133 - dev-infra/ts-circular-dependencies/config.ts | 61 - .../example-graph.png | Bin 41607 -> 0 bytes .../ts-circular-dependencies/file_system.ts | 23 - dev-infra/ts-circular-dependencies/golden.ts | 131 - dev-infra/ts-circular-dependencies/index.ts | 135 - dev-infra/ts-circular-dependencies/parser.ts | 27 - dev-infra/tsconfig.json | 8 - dev-infra/tslint-rules/BUILD.bazel | 13 - .../noImplicitOverrideAbstractRule.ts | 145 - dev-infra/utils/BUILD.bazel | 32 - dev-infra/utils/child-process.ts | 151 - dev-infra/utils/config.ts | 166 - dev-infra/utils/console.ts | 184 - dev-infra/utils/dry-run.ts | 43 - .../utils/git/authenticated-git-client.ts | 120 - dev-infra/utils/git/git-client.ts | 256 - dev-infra/utils/git/github-urls.ts | 43 - dev-infra/utils/git/github-yargs.ts | 44 - dev-infra/utils/git/github.ts | 75 - dev-infra/utils/github.ts | 85 - .../utils/inquirer-autocomplete-typings.d.ts | 17 - dev-infra/utils/semver.ts | 28 - dev-infra/utils/testing/BUILD.bazel | 16 - .../utils/testing/github-pagination-header.ts | 28 - dev-infra/utils/testing/index.ts | 11 - dev-infra/utils/testing/semver-matchers.ts | 12 - dev-infra/utils/testing/virtual-git-client.ts | 217 - .../utils/testing/virtual-git-matchers.ts | 41 - dev-infra/utils/ts-node.ts | 17 - docs/FIXUP_COMMITS.md | 4 +- modules/benchmarks/e2e_test.bzl | 2 +- .../src/change_detection/BUILD.bazel | 4 +- .../change_detection.e2e-spec.ts | 3 +- .../change_detection.perf-spec.ts | 2 +- .../transplanted_views/BUILD.bazel | 2 +- .../benchmarks/src/class_bindings/BUILD.bazel | 4 +- .../class_bindings.perf-spec.ts | 2 +- .../benchmarks/src/expanding_rows/BUILD.bazel | 8 +- .../expanding_rows.perf-spec.ts | 2 +- .../src/js-web-frameworks/BUILD.bazel | 2 +- .../js-web-frameworks.perf-spec.ts | 2 +- .../src/js-web-frameworks/ng2/BUILD.bazel | 2 +- modules/benchmarks/src/largeform/BUILD.bazel | 8 +- .../src/largeform/largeform.e2e-spec.ts | 3 +- .../src/largeform/largeform.perf-spec.ts | 3 +- .../benchmarks/src/largeform/ng2/BUILD.bazel | 2 +- modules/benchmarks/src/largetable/BUILD.bazel | 4 +- .../src/largetable/baseline/BUILD.bazel | 2 +- .../largetable/incremental_dom/BUILD.bazel | 2 +- .../benchmarks/src/largetable/iv/BUILD.bazel | 2 +- .../src/largetable/largetable.e2e-spec.ts | 3 +- .../src/largetable/largetable.perf-spec.ts | 2 +- .../benchmarks/src/largetable/ng2/BUILD.bazel | 2 +- .../src/largetable/ng2_switch/BUILD.bazel | 2 +- .../src/largetable/render3/BUILD.bazel | 6 +- modules/benchmarks/src/styling/BUILD.bazel | 6 +- .../benchmarks/src/styling/ng2/BUILD.bazel | 2 +- .../src/styling/styling_perf.spec.ts | 2 +- modules/benchmarks/src/tree/BUILD.bazel | 2 +- .../benchmarks/src/tree/baseline/BUILD.bazel | 2 +- .../src/tree/incremental_dom/BUILD.bazel | 2 +- modules/benchmarks/src/tree/iv/BUILD.bazel | 2 +- modules/benchmarks/src/tree/ng1/BUILD.bazel | 2 +- modules/benchmarks/src/tree/ng2/BUILD.bazel | 2 +- .../benchmarks/src/tree/ng2_next/BUILD.bazel | 2 +- .../src/tree/ng2_static/BUILD.bazel | 2 +- .../src/tree/ng2_switch/BUILD.bazel | 2 +- .../benchmarks/src/tree/render3/BUILD.bazel | 6 +- modules/benchmarks/src/tree/test_utils.ts | 3 +- modules/benchmarks/tsconfig.json | 2 +- .../playground/e2e_test/async/async_spec.ts | 3 +- modules/playground/e2e_test/example_test.bzl | 2 +- .../e2e_test/hello_world/hello_world_spec.ts | 3 +- modules/playground/e2e_test/http/http_spec.ts | 3 +- .../playground/e2e_test/jsonp/jsonp_spec.ts | 3 +- .../e2e_test/key_events/key_events_spec.ts | 3 +- .../model_driven_forms_spec.ts | 3 +- .../order_management/order_management_spec.ts | 3 +- .../person_management_spec.ts | 3 +- .../e2e_test/relative_assets/assets_spec.ts | 3 +- .../e2e_test/routing/routing_spec.ts | 3 +- modules/playground/e2e_test/svg/svg_spec.ts | 3 +- .../template_driven_forms_spec.ts | 3 +- .../e2e_test/upgrade/upgrade_spec.ts | 3 +- .../e2e_test/zippy_component/zippy_spec.ts | 3 +- package.json | 5 +- packages/language-service/bundles/BUILD.bazel | 2 +- packages/zone.js/test/browser/browser.spec.ts | 2 +- packages/zone.js/test/karma_test.bzl | 4 +- tools/defaults.bzl | 12 +- tools/postinstall-patches.js | 14 + tsconfig-tslint.json | 1 - tslint.json | 6 +- yarn.lock | 462 +- 323 files changed, 547 insertions(+), 26525 deletions(-) delete mode 100644 dev-infra/BUILD.bazel delete mode 100644 dev-infra/bazel/BUILD.bazel delete mode 100644 dev-infra/bazel/api-golden/BUILD.bazel delete mode 100644 dev-infra/bazel/api-golden/find_entry_points.ts delete mode 100644 dev-infra/bazel/api-golden/index.bzl delete mode 100644 dev-infra/bazel/api-golden/index.ts delete mode 100644 dev-infra/bazel/api-golden/index_npm_packages.ts delete mode 100644 dev-infra/bazel/api-golden/test_api_report.ts delete mode 100644 dev-infra/bazel/browsers/BUILD.bazel delete mode 100644 dev-infra/bazel/browsers/README.md delete mode 100644 dev-infra/bazel/browsers/browser_archive_repo.bzl delete mode 100644 dev-infra/bazel/browsers/browser_configure.bzl delete mode 100644 dev-infra/bazel/browsers/browser_repositories.bzl delete mode 100644 dev-infra/bazel/browsers/chromium/BUILD.bazel delete mode 100644 dev-infra/bazel/browsers/chromium/chromium.bzl delete mode 100644 dev-infra/bazel/browsers/chromium/chromium.json delete mode 100644 dev-infra/bazel/browsers/firefox/BUILD.bazel delete mode 100644 dev-infra/bazel/browsers/firefox/firefox.bzl delete mode 100644 dev-infra/bazel/browsers/firefox/firefox.json delete mode 100644 dev-infra/bazel/expand_template.bzl delete mode 100644 dev-infra/bazel/extract_js_module_output.bzl delete mode 100644 dev-infra/bazel/remote-execution/BUILD.bazel delete mode 100644 dev-infra/bazel/remote-execution/cpp/BUILD.bazel delete mode 100644 dev-infra/benchmark/BUILD.bazel delete mode 100644 dev-infra/benchmark/brotli-cli/BUILD.bazel delete mode 100644 dev-infra/benchmark/brotli-cli/cli.js delete mode 100644 dev-infra/benchmark/component_benchmark/BUILD.bazel delete mode 100644 dev-infra/benchmark/component_benchmark/benchmark_test.bzl delete mode 100644 dev-infra/benchmark/component_benchmark/component_benchmark.bzl delete mode 100644 dev-infra/benchmark/component_benchmark/defaults/BUILD.bazel delete mode 100644 dev-infra/benchmark/component_benchmark/defaults/index.html delete mode 100644 dev-infra/benchmark/component_benchmark/defaults/index.ts delete mode 100644 dev-infra/benchmark/component_benchmark/defaults/styles.css delete mode 100644 dev-infra/benchmark/component_benchmark/protractor-perf.conf.js delete mode 100644 dev-infra/benchmark/component_benchmark/start-server.js delete mode 100644 dev-infra/benchmark/component_benchmark/tsconfig-e2e.json delete mode 100644 dev-infra/benchmark/driver-utilities/BUILD.bazel delete mode 100644 dev-infra/benchmark/driver-utilities/e2e_util.ts delete mode 100644 dev-infra/benchmark/driver-utilities/index.ts delete mode 100644 dev-infra/benchmark/driver-utilities/perf_util.ts delete mode 100644 dev-infra/benchmark/ng_rollup_bundle/BUILD.bazel delete mode 100644 dev-infra/benchmark/ng_rollup_bundle/ng_rollup_bundle.bzl delete mode 100644 dev-infra/benchmark/ng_rollup_bundle/rollup.config-tmpl.js delete mode 100644 dev-infra/benchmark/ng_rollup_bundle/terser_config.json delete mode 100644 dev-infra/build-worker.js delete mode 100644 dev-infra/caretaker/BUILD.bazel delete mode 100644 dev-infra/caretaker/check/base.spec.ts delete mode 100644 dev-infra/caretaker/check/base.ts delete mode 100644 dev-infra/caretaker/check/check.ts delete mode 100644 dev-infra/caretaker/check/ci.spec.ts delete mode 100644 dev-infra/caretaker/check/ci.ts delete mode 100644 dev-infra/caretaker/check/cli.ts delete mode 100644 dev-infra/caretaker/check/g3.spec.ts delete mode 100644 dev-infra/caretaker/check/g3.ts delete mode 100644 dev-infra/caretaker/check/github.spec.ts delete mode 100644 dev-infra/caretaker/check/github.ts delete mode 100644 dev-infra/caretaker/check/services.spec.ts delete mode 100644 dev-infra/caretaker/check/services.ts delete mode 100644 dev-infra/caretaker/cli.ts delete mode 100644 dev-infra/caretaker/config.ts delete mode 100644 dev-infra/caretaker/handoff/cli.ts delete mode 100644 dev-infra/caretaker/handoff/update-github-team.ts delete mode 100644 dev-infra/cli.ts delete mode 120000 dev-infra/commit-message-filter.js delete mode 100644 dev-infra/commit-message/BUILD.bazel delete mode 100644 dev-infra/commit-message/cli.ts delete mode 100644 dev-infra/commit-message/config.ts delete mode 100644 dev-infra/commit-message/parse.spec.ts delete mode 100644 dev-infra/commit-message/parse.ts delete mode 100644 dev-infra/commit-message/restore-commit-message/cli.ts delete mode 100644 dev-infra/commit-message/restore-commit-message/commit-message-draft.ts delete mode 100644 dev-infra/commit-message/restore-commit-message/commit-message-source.ts delete mode 100644 dev-infra/commit-message/restore-commit-message/restore-commit-message.ts delete mode 100644 dev-infra/commit-message/test-util.ts delete mode 100644 dev-infra/commit-message/utils.ts delete mode 100644 dev-infra/commit-message/validate-file/cli.ts delete mode 100644 dev-infra/commit-message/validate-file/validate-file.ts delete mode 100644 dev-infra/commit-message/validate-range/cli.ts delete mode 100644 dev-infra/commit-message/validate-range/validate-range.ts delete mode 100644 dev-infra/commit-message/validate.spec.ts delete mode 100644 dev-infra/commit-message/validate.ts delete mode 100644 dev-infra/defaults.bzl delete mode 100644 dev-infra/format/BUILD.bazel delete mode 100644 dev-infra/format/cli.ts delete mode 100644 dev-infra/format/config.ts delete mode 100644 dev-infra/format/format.ts delete mode 100644 dev-infra/format/formatters/base-formatter.ts delete mode 100644 dev-infra/format/formatters/buildifier.ts delete mode 100644 dev-infra/format/formatters/clang-format.ts delete mode 100644 dev-infra/format/formatters/index.ts delete mode 100644 dev-infra/format/formatters/prettier.ts delete mode 100644 dev-infra/format/run-commands-parallel.ts delete mode 100644 dev-infra/index.bzl delete mode 100644 dev-infra/misc/BUILD.bazel delete mode 100644 dev-infra/misc/build-and-link/cli.ts delete mode 100644 dev-infra/misc/cli.ts delete mode 100755 dev-infra/ng-dev.js delete mode 100644 dev-infra/ngbot/BUILD.bazel delete mode 100644 dev-infra/ngbot/cli.ts delete mode 100644 dev-infra/ngbot/verify.ts delete mode 100644 dev-infra/pr/BUILD.bazel delete mode 100644 dev-infra/pr/check-target-branches/BUILD.bazel delete mode 100644 dev-infra/pr/check-target-branches/check-target-branches.ts delete mode 100644 dev-infra/pr/check-target-branches/cli.ts delete mode 100644 dev-infra/pr/checkout/BUILD.bazel delete mode 100644 dev-infra/pr/checkout/cli.ts delete mode 100644 dev-infra/pr/cli.ts delete mode 100644 dev-infra/pr/common/BUILD.bazel delete mode 100644 dev-infra/pr/common/checkout-pr.ts delete mode 100644 dev-infra/pr/discover-new-conflicts/BUILD.bazel delete mode 100644 dev-infra/pr/discover-new-conflicts/cli.ts delete mode 100644 dev-infra/pr/discover-new-conflicts/index.ts delete mode 100644 dev-infra/pr/merge/BUILD.bazel delete mode 100644 dev-infra/pr/merge/cli.ts delete mode 100644 dev-infra/pr/merge/config.ts delete mode 100644 dev-infra/pr/merge/defaults/index.ts delete mode 100644 dev-infra/pr/merge/defaults/integration.spec.ts delete mode 100644 dev-infra/pr/merge/defaults/labels.ts delete mode 100644 dev-infra/pr/merge/defaults/lts-branch.ts delete mode 100644 dev-infra/pr/merge/failures.ts delete mode 100644 dev-infra/pr/merge/index.ts delete mode 100644 dev-infra/pr/merge/messages.ts delete mode 100644 dev-infra/pr/merge/pull-request.ts delete mode 100644 dev-infra/pr/merge/strategies/api-merge.ts delete mode 100644 dev-infra/pr/merge/strategies/autosquash-merge.ts delete mode 100755 dev-infra/pr/merge/strategies/commit-message-filter.js delete mode 100644 dev-infra/pr/merge/strategies/strategy.ts delete mode 100644 dev-infra/pr/merge/string-pattern.ts delete mode 100644 dev-infra/pr/merge/target-label.ts delete mode 100644 dev-infra/pr/merge/task.ts delete mode 100644 dev-infra/pr/rebase/BUILD.bazel delete mode 100644 dev-infra/pr/rebase/cli.ts delete mode 100644 dev-infra/pr/rebase/index.ts delete mode 100644 dev-infra/pullapprove/BUILD.bazel delete mode 100644 dev-infra/pullapprove/cli.ts delete mode 100644 dev-infra/pullapprove/condition_evaluator.ts delete mode 100644 dev-infra/pullapprove/group.ts delete mode 100644 dev-infra/pullapprove/logging.ts delete mode 100644 dev-infra/pullapprove/parse-yaml.ts delete mode 100644 dev-infra/pullapprove/pullapprove_arrays.ts delete mode 100644 dev-infra/pullapprove/utils.ts delete mode 100644 dev-infra/pullapprove/verify.spec.ts delete mode 100644 dev-infra/pullapprove/verify.ts delete mode 100644 dev-infra/release/BUILD.bazel delete mode 100644 dev-infra/release/build/BUILD.bazel delete mode 100644 dev-infra/release/build/build-worker.ts delete mode 100644 dev-infra/release/build/build.spec.ts delete mode 100644 dev-infra/release/build/cli.ts delete mode 100644 dev-infra/release/build/index.ts delete mode 100644 dev-infra/release/cli.ts delete mode 100644 dev-infra/release/config/BUILD.bazel delete mode 100644 dev-infra/release/config/index.ts delete mode 100644 dev-infra/release/info/BUILD.bazel delete mode 100644 dev-infra/release/info/cli.ts delete mode 100644 dev-infra/release/notes/BUILD.bazel delete mode 100644 dev-infra/release/notes/cli.ts delete mode 100644 dev-infra/release/notes/context.ts delete mode 100644 dev-infra/release/notes/release-notes.ts delete mode 100644 dev-infra/release/notes/templates/changelog.ts delete mode 100644 dev-infra/release/notes/templates/github-release.ts delete mode 100644 dev-infra/release/publish/BUILD.bazel delete mode 100644 dev-infra/release/publish/actions-error.ts delete mode 100644 dev-infra/release/publish/actions.ts delete mode 100644 dev-infra/release/publish/actions/branch-off-next-branch.ts delete mode 100644 dev-infra/release/publish/actions/configure-next-as-major.ts delete mode 100644 dev-infra/release/publish/actions/cut-lts-patch.ts delete mode 100644 dev-infra/release/publish/actions/cut-new-patch.ts delete mode 100644 dev-infra/release/publish/actions/cut-next-prerelease.ts delete mode 100644 dev-infra/release/publish/actions/cut-release-candidate-for-feature-freeze.ts delete mode 100644 dev-infra/release/publish/actions/cut-stable.ts delete mode 100644 dev-infra/release/publish/actions/index.ts delete mode 100644 dev-infra/release/publish/actions/move-next-into-feature-freeze.ts delete mode 100644 dev-infra/release/publish/actions/move-next-into-release-candidate.ts delete mode 100644 dev-infra/release/publish/actions/tag-recent-major-as-latest.ts delete mode 100644 dev-infra/release/publish/cli.ts delete mode 100644 dev-infra/release/publish/commit-message.ts delete mode 100644 dev-infra/release/publish/constants.ts delete mode 100644 dev-infra/release/publish/external-commands.ts delete mode 100644 dev-infra/release/publish/graphql-queries.ts delete mode 100644 dev-infra/release/publish/index.ts delete mode 100644 dev-infra/release/publish/pull-request-state.ts delete mode 100644 dev-infra/release/publish/test/BUILD.bazel delete mode 100644 dev-infra/release/publish/test/branch-off-next-branch-testing.ts delete mode 100644 dev-infra/release/publish/test/common.spec.ts delete mode 100644 dev-infra/release/publish/test/configure-next-as-major.spec.ts delete mode 100644 dev-infra/release/publish/test/cut-lts-patch.spec.ts delete mode 100644 dev-infra/release/publish/test/cut-new-patch.spec.ts delete mode 100644 dev-infra/release/publish/test/cut-next-prerelease.spec.ts delete mode 100644 dev-infra/release/publish/test/cut-release-candidate-for-feature-freeze.spec.ts delete mode 100644 dev-infra/release/publish/test/cut-stable.spec.ts delete mode 100644 dev-infra/release/publish/test/github-api-testing.ts delete mode 100644 dev-infra/release/publish/test/move-next-into-feature-freeze.spec.ts delete mode 100644 dev-infra/release/publish/test/move-next-into-release-candidate.spec.ts delete mode 100644 dev-infra/release/publish/test/release-notes/context.spec.ts delete mode 100644 dev-infra/release/publish/test/release-notes/release-notes-utils.ts delete mode 100644 dev-infra/release/publish/test/tag-recent-major-as-latest.spec.ts delete mode 100644 dev-infra/release/publish/test/test-utils.ts delete mode 100644 dev-infra/release/set-dist-tag/BUILD.bazel delete mode 100644 dev-infra/release/set-dist-tag/cli.ts delete mode 100644 dev-infra/release/set-dist-tag/set-dist-tag.spec.ts delete mode 100644 dev-infra/release/stamping/cli.ts delete mode 100644 dev-infra/release/stamping/env-stamp.ts delete mode 100644 dev-infra/release/versioning/BUILD.bazel delete mode 100644 dev-infra/release/versioning/README.md delete mode 100644 dev-infra/release/versioning/active-release-trains.ts delete mode 100644 dev-infra/release/versioning/index.ts delete mode 100644 dev-infra/release/versioning/long-term-support.ts delete mode 100644 dev-infra/release/versioning/next-prerelease-version.ts delete mode 100644 dev-infra/release/versioning/npm-publish.ts delete mode 100644 dev-infra/release/versioning/npm-registry.ts delete mode 100644 dev-infra/release/versioning/print-active-trains.ts delete mode 100644 dev-infra/release/versioning/release-trains.ts delete mode 100644 dev-infra/release/versioning/version-branches.ts delete mode 100644 dev-infra/tmpl-package.json delete mode 100644 dev-infra/ts-circular-dependencies/BUILD.bazel delete mode 100644 dev-infra/ts-circular-dependencies/README.md delete mode 100644 dev-infra/ts-circular-dependencies/analyzer.ts delete mode 100644 dev-infra/ts-circular-dependencies/config.ts delete mode 100644 dev-infra/ts-circular-dependencies/example-graph.png delete mode 100644 dev-infra/ts-circular-dependencies/file_system.ts delete mode 100644 dev-infra/ts-circular-dependencies/golden.ts delete mode 100644 dev-infra/ts-circular-dependencies/index.ts delete mode 100644 dev-infra/ts-circular-dependencies/parser.ts delete mode 100644 dev-infra/tsconfig.json delete mode 100644 dev-infra/tslint-rules/BUILD.bazel delete mode 100644 dev-infra/tslint-rules/noImplicitOverrideAbstractRule.ts delete mode 100644 dev-infra/utils/BUILD.bazel delete mode 100644 dev-infra/utils/child-process.ts delete mode 100644 dev-infra/utils/config.ts delete mode 100644 dev-infra/utils/console.ts delete mode 100644 dev-infra/utils/dry-run.ts delete mode 100644 dev-infra/utils/git/authenticated-git-client.ts delete mode 100644 dev-infra/utils/git/git-client.ts delete mode 100644 dev-infra/utils/git/github-urls.ts delete mode 100644 dev-infra/utils/git/github-yargs.ts delete mode 100644 dev-infra/utils/git/github.ts delete mode 100644 dev-infra/utils/github.ts delete mode 100644 dev-infra/utils/inquirer-autocomplete-typings.d.ts delete mode 100644 dev-infra/utils/semver.ts delete mode 100644 dev-infra/utils/testing/BUILD.bazel delete mode 100644 dev-infra/utils/testing/github-pagination-header.ts delete mode 100644 dev-infra/utils/testing/index.ts delete mode 100644 dev-infra/utils/testing/semver-matchers.ts delete mode 100644 dev-infra/utils/testing/virtual-git-client.ts delete mode 100644 dev-infra/utils/testing/virtual-git-matchers.ts delete mode 100644 dev-infra/utils/ts-node.ts diff --git a/.bazelrc b/.bazelrc index 8e287183060576..d0f63e8c11a5d4 100644 --- a/.bazelrc +++ b/.bazelrc @@ -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 diff --git a/.github/angular-robot.yml b/.github/angular-robot.yml index 04c72688c1b852..39c903cbe32564 100644 --- a/.github/angular-robot.yml +++ b/.github/angular-robot.yml @@ -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/*' diff --git a/.gitmessage b/.gitmessage index 536045e1d90f69..c607c42e74a9ee 100644 --- a/.gitmessage +++ b/.gitmessage @@ -106,8 +106,7 @@ Fixes # # │ └─⫸ 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 diff --git a/.ng-dev/caretaker.ts b/.ng-dev/caretaker.ts index f922bd7f5c3fe1..1b2a720e7a388c 100644 --- a/.ng-dev/caretaker.ts +++ b/.ng-dev/caretaker.ts @@ -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 = { diff --git a/.ng-dev/commit-message.ts b/.ng-dev/commit-message.ts index 4a74d244d7838a..13da4dc5ae203a 100644 --- a/.ng-dev/commit-message.ts +++ b/.ng-dev/commit-message.ts @@ -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. diff --git a/.ng-dev/format.ts b/.ng-dev/format.ts index 2dfdcd23346442..a729d4201f6446 100644 --- a/.ng-dev/format.ts +++ b/.ng-dev/format.ts @@ -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. diff --git a/.ng-dev/github.ts b/.ng-dev/github.ts index c9b009bea57236..3425151c94554c 100644 --- a/.ng-dev/github.ts +++ b/.ng-dev/github.ts @@ -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 diff --git a/.ng-dev/merge.ts b/.ng-dev/merge.ts index 729db06f367d1a..4ebfb9957012a1 100644 --- a/.ng-dev/merge.ts +++ b/.ng-dev/merge.ts @@ -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'; diff --git a/.ng-dev/release.ts b/.ng-dev/release.ts index dbb836175b6598..ec31bd1c077bde 100644 --- a/.ng-dev/release.ts +++ b/.ng-dev/release.ts @@ -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 = { diff --git a/.pullapprove.yml b/.pullapprove.yml index 74d3d82d928b16..2c4ae1fa019140 100644 --- a/.pullapprove.yml +++ b/.pullapprove.yml @@ -1155,7 +1155,6 @@ groups: '.ng-dev/**', '.vscode/**', '.yarn/**', - 'dev-infra/**', 'docs/*.md', 'docs/images/**', 'goldens/*', diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9a8ba801c23587..bf9c0ad45de4a2 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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 diff --git a/WORKSPACE b/WORKSPACE index a79fffe8a8cff0..e09c90faf9d0ca 100644 --- a/WORKSPACE +++ b/WORKSPACE @@ -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() diff --git a/dev-infra/BUILD.bazel b/dev-infra/BUILD.bazel deleted file mode 100644 index adf143a33cfe6e..00000000000000 --- a/dev-infra/BUILD.bazel +++ /dev/null @@ -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 diff --git a/dev-infra/bazel/BUILD.bazel b/dev-infra/bazel/BUILD.bazel deleted file mode 100644 index b8e78fe5d50931..00000000000000 --- a/dev-infra/bazel/BUILD.bazel +++ /dev/null @@ -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", - ], -) diff --git a/dev-infra/bazel/api-golden/BUILD.bazel b/dev-infra/bazel/api-golden/BUILD.bazel deleted file mode 100644 index 1c417b7d2c6a12..00000000000000 --- a/dev-infra/bazel/api-golden/BUILD.bazel +++ /dev/null @@ -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(["*"]), -) diff --git a/dev-infra/bazel/api-golden/find_entry_points.ts b/dev-infra/bazel/api-golden/find_entry_points.ts deleted file mode 100644 index 3343639fecbadb..00000000000000 --- a/dev-infra/bazel/api-golden/find_entry_points.ts +++ /dev/null @@ -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 { - for (const fileName of readdirSync(directoryPath)) { - const fullPath = join(directoryPath, fileName); - if (isDirectory(fullPath)) { - yield* findPackageJsonFilesInDirectory(fullPath); - } else if (fileName === 'package.json') { - yield fullPath; - } - } -} diff --git a/dev-infra/bazel/api-golden/index.bzl b/dev-infra/bazel/api-golden/index.bzl deleted file mode 100644 index 6f2142fbe976ca..00000000000000 --- a/dev-infra/bazel/api-golden/index.bzl +++ /dev/null @@ -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: " + - "`@//@types/`, 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 - ) diff --git a/dev-infra/bazel/api-golden/index.ts b/dev-infra/bazel/api-golden/index.ts deleted file mode 100644 index 3de18aa7ca0b74..00000000000000 --- a/dev-infra/bazel/api-golden/index.ts +++ /dev/null @@ -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); - }); -} diff --git a/dev-infra/bazel/api-golden/index_npm_packages.ts b/dev-infra/bazel/api-golden/index_npm_packages.ts deleted file mode 100644 index 20dc2a4d3ddc5d..00000000000000 --- a/dev-infra/bazel/api-golden/index_npm_packages.ts +++ /dev/null @@ -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); - }); -} diff --git a/dev-infra/bazel/api-golden/test_api_report.ts b/dev-infra/bazel/api-golden/test_api_report.ts deleted file mode 100644 index 45d82713c06efa..00000000000000 --- a/dev-infra/bazel/api-golden/test_api_report.ts +++ /dev/null @@ -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 { - // 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`); -} diff --git a/dev-infra/bazel/browsers/BUILD.bazel b/dev-infra/bazel/browsers/BUILD.bazel deleted file mode 100644 index dabe4af6da365f..00000000000000 --- a/dev-infra/bazel/browsers/BUILD.bazel +++ /dev/null @@ -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", - ], -) diff --git a/dev-infra/bazel/browsers/README.md b/dev-infra/bazel/browsers/README.md deleted file mode 100644 index 618146cbb1ea13..00000000000000 --- a/dev-infra/bazel/browsers/README.md +++ /dev/null @@ -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 | 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 | shasum -a 256 - ``` diff --git a/dev-infra/bazel/browsers/browser_archive_repo.bzl b/dev-infra/bazel/browsers/browser_archive_repo.bzl deleted file mode 100644 index c51c40336e84c9..00000000000000 --- a/dev-infra/bazel/browsers/browser_archive_repo.bzl +++ /dev/null @@ -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, - ), - }, -) diff --git a/dev-infra/bazel/browsers/browser_configure.bzl b/dev-infra/bazel/browsers/browser_configure.bzl deleted file mode 100644 index ba184dcdce26e8..00000000000000 --- a/dev-infra/bazel/browsers/browser_configure.bzl +++ /dev/null @@ -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, -) diff --git a/dev-infra/bazel/browsers/browser_repositories.bzl b/dev-infra/bazel/browsers/browser_repositories.bzl deleted file mode 100644 index cd65f89abc61fb..00000000000000 --- a/dev-infra/bazel/browsers/browser_repositories.bzl +++ /dev/null @@ -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() diff --git a/dev-infra/bazel/browsers/chromium/BUILD.bazel b/dev-infra/bazel/browsers/chromium/BUILD.bazel deleted file mode 100644 index 6f5d4077eb96a2..00000000000000 --- a/dev-infra/bazel/browsers/chromium/BUILD.bazel +++ /dev/null @@ -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(["*"]), -) diff --git a/dev-infra/bazel/browsers/chromium/chromium.bzl b/dev-infra/bazel/browsers/chromium/chromium.bzl deleted file mode 100644 index 2af38af7bb8f35..00000000000000 --- a/dev-infra/bazel/browsers/chromium/chromium.bzl +++ /dev/null @@ -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", - }, - ) diff --git a/dev-infra/bazel/browsers/chromium/chromium.json b/dev-infra/bazel/browsers/chromium/chromium.json deleted file mode 100644 index 7fb23d3486695c..00000000000000 --- a/dev-infra/bazel/browsers/chromium/chromium.json +++ /dev/null @@ -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 - } - } -} diff --git a/dev-infra/bazel/browsers/firefox/BUILD.bazel b/dev-infra/bazel/browsers/firefox/BUILD.bazel deleted file mode 100644 index 4c2ed68df628ca..00000000000000 --- a/dev-infra/bazel/browsers/firefox/BUILD.bazel +++ /dev/null @@ -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(["*"]), -) diff --git a/dev-infra/bazel/browsers/firefox/firefox.bzl b/dev-infra/bazel/browsers/firefox/firefox.bzl deleted file mode 100644 index c959703fec8310..00000000000000 --- a/dev-infra/bazel/browsers/firefox/firefox.bzl +++ /dev/null @@ -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", - }, - ) diff --git a/dev-infra/bazel/browsers/firefox/firefox.json b/dev-infra/bazel/browsers/firefox/firefox.json deleted file mode 100644 index 0cbc7af4a3aca6..00000000000000 --- a/dev-infra/bazel/browsers/firefox/firefox.json +++ /dev/null @@ -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 - } - } -} diff --git a/dev-infra/bazel/expand_template.bzl b/dev-infra/bazel/expand_template.bzl deleted file mode 100644 index e21bd9fa1d8357..00000000000000 --- a/dev-infra/bazel/expand_template.bzl +++ /dev/null @@ -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.", - ), - }, -) diff --git a/dev-infra/bazel/extract_js_module_output.bzl b/dev-infra/bazel/extract_js_module_output.bzl deleted file mode 100644 index 42ca77f738f67d..00000000000000 --- a/dev-infra/bazel/extract_js_module_output.bzl +++ /dev/null @@ -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. - """, - ), - }, -) diff --git a/dev-infra/bazel/remote-execution/BUILD.bazel b/dev-infra/bazel/remote-execution/BUILD.bazel deleted file mode 100644 index 718894068c207a..00000000000000 --- a/dev-infra/bazel/remote-execution/BUILD.bazel +++ /dev/null @@ -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", - ], -) diff --git a/dev-infra/bazel/remote-execution/cpp/BUILD.bazel b/dev-infra/bazel/remote-execution/cpp/BUILD.bazel deleted file mode 100644 index 83c3fda085bfbb..00000000000000 --- a/dev-infra/bazel/remote-execution/cpp/BUILD.bazel +++ /dev/null @@ -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") diff --git a/dev-infra/benchmark/BUILD.bazel b/dev-infra/benchmark/BUILD.bazel deleted file mode 100644 index d56acdbef7a4d1..00000000000000 --- a/dev-infra/benchmark/BUILD.bazel +++ /dev/null @@ -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", - ], -) diff --git a/dev-infra/benchmark/brotli-cli/BUILD.bazel b/dev-infra/benchmark/brotli-cli/BUILD.bazel deleted file mode 100644 index a97d1f5e19c1b5..00000000000000 --- a/dev-infra/benchmark/brotli-cli/BUILD.bazel +++ /dev/null @@ -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(["*"]), -) diff --git a/dev-infra/benchmark/brotli-cli/cli.js b/dev-infra/benchmark/brotli-cli/cli.js deleted file mode 100644 index 0c08cbf5b7b619..00000000000000 --- a/dev-infra/benchmark/brotli-cli/cli.js +++ /dev/null @@ -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)); -} diff --git a/dev-infra/benchmark/component_benchmark/BUILD.bazel b/dev-infra/benchmark/component_benchmark/BUILD.bazel deleted file mode 100644 index 4a4596f070feb8..00000000000000 --- a/dev-infra/benchmark/component_benchmark/BUILD.bazel +++ /dev/null @@ -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"], -) diff --git a/dev-infra/benchmark/component_benchmark/benchmark_test.bzl b/dev-infra/benchmark/component_benchmark/benchmark_test.bzl deleted file mode 100644 index e0fca3dfcee5f1..00000000000000 --- a/dev-infra/benchmark/component_benchmark/benchmark_test.bzl +++ /dev/null @@ -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 - ) diff --git a/dev-infra/benchmark/component_benchmark/component_benchmark.bzl b/dev-infra/benchmark/component_benchmark/component_benchmark.bzl deleted file mode 100644 index 4076a0f363b7c4..00000000000000 --- a/dev-infra/benchmark/component_benchmark/component_benchmark.bzl +++ /dev/null @@ -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], - ) diff --git a/dev-infra/benchmark/component_benchmark/defaults/BUILD.bazel b/dev-infra/benchmark/component_benchmark/defaults/BUILD.bazel deleted file mode 100644 index 0eb8038377aa38..00000000000000 --- a/dev-infra/benchmark/component_benchmark/defaults/BUILD.bazel +++ /dev/null @@ -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", -]) diff --git a/dev-infra/benchmark/component_benchmark/defaults/index.html b/dev-infra/benchmark/component_benchmark/defaults/index.html deleted file mode 100644 index f1fdc71047091c..00000000000000 --- a/dev-infra/benchmark/component_benchmark/defaults/index.html +++ /dev/null @@ -1,14 +0,0 @@ - - - - - Benchmark Test - - - - - - Loading... - - - diff --git a/dev-infra/benchmark/component_benchmark/defaults/index.ts b/dev-infra/benchmark/component_benchmark/defaults/index.ts deleted file mode 100644 index 4186e824e5d567..00000000000000 --- a/dev-infra/benchmark/component_benchmark/defaults/index.ts +++ /dev/null @@ -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); diff --git a/dev-infra/benchmark/component_benchmark/defaults/styles.css b/dev-infra/benchmark/component_benchmark/defaults/styles.css deleted file mode 100644 index 04bd77b44b6e60..00000000000000 --- a/dev-infra/benchmark/component_benchmark/defaults/styles.css +++ /dev/null @@ -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. - */ diff --git a/dev-infra/benchmark/component_benchmark/protractor-perf.conf.js b/dev-infra/benchmark/component_benchmark/protractor-perf.conf.js deleted file mode 100644 index 8ccf197b546b77..00000000000000 --- a/dev-infra/benchmark/component_benchmark/protractor-perf.conf.js +++ /dev/null @@ -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 -}; diff --git a/dev-infra/benchmark/component_benchmark/start-server.js b/dev-infra/benchmark/component_benchmark/start-server.js deleted file mode 100644 index 2dacb55428bd03..00000000000000 --- a/dev-infra/benchmark/component_benchmark/start-server.js +++ /dev/null @@ -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; -}; diff --git a/dev-infra/benchmark/component_benchmark/tsconfig-e2e.json b/dev-infra/benchmark/component_benchmark/tsconfig-e2e.json deleted file mode 100644 index ec8572b587b5cf..00000000000000 --- a/dev-infra/benchmark/component_benchmark/tsconfig-e2e.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "compilerOptions": { - "lib": ["es2015", "dom"], - "types": ["node", "jasmine"] - } -} diff --git a/dev-infra/benchmark/driver-utilities/BUILD.bazel b/dev-infra/benchmark/driver-utilities/BUILD.bazel deleted file mode 100644 index 3ef04bd319b80f..00000000000000 --- a/dev-infra/benchmark/driver-utilities/BUILD.bazel +++ /dev/null @@ -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", - ], -) diff --git a/dev-infra/benchmark/driver-utilities/e2e_util.ts b/dev-infra/benchmark/driver-utilities/e2e_util.ts deleted file mode 100644 index 3b391f26e4844b..00000000000000 --- a/dev-infra/benchmark/driver-utilities/e2e_util.ts +++ /dev/null @@ -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([]); - }); -} diff --git a/dev-infra/benchmark/driver-utilities/index.ts b/dev-infra/benchmark/driver-utilities/index.ts deleted file mode 100644 index 979bdf35007064..00000000000000 --- a/dev-infra/benchmark/driver-utilities/index.ts +++ /dev/null @@ -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'; diff --git a/dev-infra/benchmark/driver-utilities/perf_util.ts b/dev-infra/benchmark/driver-utilities/perf_util.ts deleted file mode 100644 index a9043b9a2467a6..00000000000000 --- a/dev-infra/benchmark/driver-utilities/perf_util.ts +++ /dev/null @@ -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), - prepare?: (() => void)|(() => Promise), - setup?: (() => void)|(() => Promise), -}): Promise { - 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); -} diff --git a/dev-infra/benchmark/ng_rollup_bundle/BUILD.bazel b/dev-infra/benchmark/ng_rollup_bundle/BUILD.bazel deleted file mode 100644 index afb8a7a6951717..00000000000000 --- a/dev-infra/benchmark/ng_rollup_bundle/BUILD.bazel +++ /dev/null @@ -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(["*"]), -) diff --git a/dev-infra/benchmark/ng_rollup_bundle/ng_rollup_bundle.bzl b/dev-infra/benchmark/ng_rollup_bundle/ng_rollup_bundle.bzl deleted file mode 100644 index 166fb1bb218615..00000000000000 --- a/dev-infra/benchmark/ng_rollup_bundle/ng_rollup_bundle.bzl +++ /dev/null @@ -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 "/<..>/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, - ) diff --git a/dev-infra/benchmark/ng_rollup_bundle/rollup.config-tmpl.js b/dev-infra/benchmark/ng_rollup_bundle/rollup.config-tmpl.js deleted file mode 100644 index ab45b6fe6b45be..00000000000000 --- a/dev-infra/benchmark/ng_rollup_bundle/rollup.config-tmpl.js +++ /dev/null @@ -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; -} diff --git a/dev-infra/benchmark/ng_rollup_bundle/terser_config.json b/dev-infra/benchmark/ng_rollup_bundle/terser_config.json deleted file mode 100644 index fcb6a31ad47c31..00000000000000 --- a/dev-infra/benchmark/ng_rollup_bundle/terser_config.json +++ /dev/null @@ -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" -} diff --git a/dev-infra/build-worker.js b/dev-infra/build-worker.js deleted file mode 100644 index 19a062e471e43a..00000000000000 --- a/dev-infra/build-worker.js +++ /dev/null @@ -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); - }); -} diff --git a/dev-infra/caretaker/BUILD.bazel b/dev-infra/caretaker/BUILD.bazel deleted file mode 100644 index 361b4c9ccec557..00000000000000 --- a/dev-infra/caretaker/BUILD.bazel +++ /dev/null @@ -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", - ], -) diff --git a/dev-infra/caretaker/check/base.spec.ts b/dev-infra/caretaker/check/base.spec.ts deleted file mode 100644 index 0850d111173d1c..00000000000000 --- a/dev-infra/caretaker/check/base.spec.ts +++ /dev/null @@ -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 { - 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); - }); -}); diff --git a/dev-infra/caretaker/check/base.ts b/dev-infra/caretaker/check/base.ts deleted file mode 100644 index 61ca5bbccbf858..00000000000000 --- a/dev-infra/caretaker/check/base.ts +++ /dev/null @@ -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 { - /** 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; - - /** Print the information discovered for the module to the terminal. */ - abstract printToTerminal(): Promise; -} diff --git a/dev-infra/caretaker/check/check.ts b/dev-infra/caretaker/check/check.ts deleted file mode 100644 index d4c4396be18fee..00000000000000 --- a/dev-infra/caretaker/check/check.ts +++ /dev/null @@ -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 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)); - - for (const module of caretakerCheckModules) { - await module.printToTerminal(); - } -} diff --git a/dev-infra/caretaker/check/ci.spec.ts b/dev-infra/caretaker/check/ci.spec.ts deleted file mode 100644 index 4b21f0968b35e4..00000000000000 --- a/dev-infra/caretaker/check/ci.spec.ts +++ /dev/null @@ -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} - }; -} diff --git a/dev-infra/caretaker/check/ci.ts b/dev-infra/caretaker/check/ci.ts deleted file mode 100644 index 756a6ef32628dd..00000000000000 --- a/dev-infra/caretaker/check/ci.ts +++ /dev/null @@ -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 { - 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 { - 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'; - } -} diff --git a/dev-infra/caretaker/check/cli.ts b/dev-infra/caretaker/check/cli.ts deleted file mode 100644 index 92343638ecb281..00000000000000 --- a/dev-infra/caretaker/check/cli.ts +++ /dev/null @@ -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', -}; diff --git a/dev-infra/caretaker/check/g3.spec.ts b/dev-infra/caretaker/check/g3.spec.ts deleted file mode 100644 index bd3483d485564b..00000000000000 --- a/dev-infra/caretaker/check/g3.spec.ts +++ /dev/null @@ -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> = {}; - 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'); - }); - }); -}); diff --git a/dev-infra/caretaker/check/g3.ts b/dev-infra/caretaker/check/g3.ts deleted file mode 100644 index 07349e50fd1ed3..00000000000000 --- a/dev-infra/caretaker/check/g3.ts +++ /dev/null @@ -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 { - 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}; - } -} diff --git a/dev-infra/caretaker/check/github.spec.ts b/dev-infra/caretaker/check/github.spec.ts deleted file mode 100644 index 08af371533bc91..00000000000000 --- a/dev-infra/caretaker/check/github.spec.ts +++ /dev/null @@ -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'); - }); - }); -}); diff --git a/dev-infra/caretaker/check/github.ts b/dev-infra/caretaker/check/github.ts deleted file mode 100644 index 7075ce2ed097a5..00000000000000 --- a/dev-infra/caretaker/check/github.ts +++ /dev/null @@ -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 { - 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) { - /** 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(); - } -} diff --git a/dev-infra/caretaker/check/services.spec.ts b/dev-infra/caretaker/check/services.spec.ts deleted file mode 100644 index 6c4688fe7d8226..00000000000000 --- a/dev-infra/caretaker/check/services.spec.ts +++ /dev/null @@ -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'); - }); - }); -}); diff --git a/dev-infra/caretaker/check/services.ts b/dev-infra/caretaker/check/services.ts deleted file mode 100644 index 46364263a36635..00000000000000 --- a/dev-infra/caretaker/check/services.ts +++ /dev/null @@ -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 { - 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 { - 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) - }; - } -} diff --git a/dev-infra/caretaker/cli.ts b/dev-infra/caretaker/cli.ts deleted file mode 100644 index 8a589c43db5a34..00000000000000 --- a/dev-infra/caretaker/cli.ts +++ /dev/null @@ -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); -} diff --git a/dev-infra/caretaker/config.ts b/dev-infra/caretaker/config.ts deleted file mode 100644 index 027158e1e39a6a..00000000000000 --- a/dev-infra/caretaker/config.ts +++ /dev/null @@ -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 "-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> = getConfig(); - - assertNoErrors(errors); - return config as Required; -} diff --git a/dev-infra/caretaker/handoff/cli.ts b/dev-infra/caretaker/handoff/cli.ts deleted file mode 100644 index a59fc824803efe..00000000000000 --- a/dev-infra/caretaker/handoff/cli.ts +++ /dev/null @@ -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', -}; diff --git a/dev-infra/caretaker/handoff/update-github-team.ts b/dev-infra/caretaker/handoff/update-github-team.ts deleted file mode 100644 index 337ff7d2583d63..00000000000000 --- a/dev-infra/caretaker/handoff/update-github-team.ts +++ /dev/null @@ -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 /. */ - 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}`); -} diff --git a/dev-infra/cli.ts b/dev-infra/cli.ts deleted file mode 100644 index 6bfd94274ef248..00000000000000 --- a/dev-infra/cli.ts +++ /dev/null @@ -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 ', '', buildCommitMessageParser) - .command('format ', '', buildFormatParser) - .command('pr ', '', buildPrParser) - .command('pullapprove ', '', buildPullapproveParser) - .command('release ', '', buildReleaseParser) - .command('ts-circular-deps ', '', tsCircularDependenciesBuilder) - .command('caretaker ', '', buildCaretakerParser) - .command('misc ', '', buildMiscParser) - .command('ngbot ', false, buildNgbotParser) - .wrap(120) - .strict() - .parse(); diff --git a/dev-infra/commit-message-filter.js b/dev-infra/commit-message-filter.js deleted file mode 120000 index abb734a839c2b1..00000000000000 --- a/dev-infra/commit-message-filter.js +++ /dev/null @@ -1 +0,0 @@ -pr/merge/strategies/commit-message-filter.js \ No newline at end of file diff --git a/dev-infra/commit-message/BUILD.bazel b/dev-infra/commit-message/BUILD.bazel deleted file mode 100644 index e0299445f8fd0c..00000000000000 --- a/dev-infra/commit-message/BUILD.bazel +++ /dev/null @@ -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", - ], -) diff --git a/dev-infra/commit-message/cli.ts b/dev-infra/commit-message/cli.ts deleted file mode 100644 index 0ed65b71e3d295..00000000000000 --- a/dev-infra/commit-message/cli.ts +++ /dev/null @@ -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); -} diff --git a/dev-infra/commit-message/config.ts b/dev-infra/commit-message/config.ts deleted file mode 100644 index e0f24a5bf728f0..00000000000000 --- a/dev-infra/commit-message/config.ts +++ /dev/null @@ -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> = getConfig(); - - if (config.commitMessage === undefined) { - errors.push(`No configuration defined for "commitMessage"`); - } - - assertNoErrors(errors); - return config as Required; -} - -/** 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, - }, -}; diff --git a/dev-infra/commit-message/parse.spec.ts b/dev-infra/commit-message/parse.spec.ts deleted file mode 100644 index e7f6924fa6d77f..00000000000000 --- a/dev-infra/commit-message/parse.spec.ts +++ /dev/null @@ -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); - }); - }); -}); diff --git a/dev-infra/commit-message/parse.ts b/dev-infra/commit-message/parse.ts deleted file mode 100644 index 0cebcc891365e1..00000000000000 --- a/dev-infra/commit-message/parse.ts +++ /dev/null @@ -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, - }; -} diff --git a/dev-infra/commit-message/restore-commit-message/cli.ts b/dev-infra/commit-message/restore-commit-message/cli.ts deleted file mode 100644 index 785ecaaa77802e..00000000000000 --- a/dev-infra/commit-message/restore-commit-message/cli.ts +++ /dev/null @@ -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) { - // 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, -}; diff --git a/dev-infra/commit-message/restore-commit-message/commit-message-draft.ts b/dev-infra/commit-message/restore-commit-message/commit-message-draft.ts deleted file mode 100644 index 86a5655fd09b09..00000000000000 --- a/dev-infra/commit-message/restore-commit-message/commit-message-draft.ts +++ /dev/null @@ -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); -} diff --git a/dev-infra/commit-message/restore-commit-message/commit-message-source.ts b/dev-infra/commit-message/restore-commit-message/commit-message-source.ts deleted file mode 100644 index c8a9640e839edc..00000000000000 --- a/dev-infra/commit-message/restore-commit-message/commit-message-source.ts +++ /dev/null @@ -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'; diff --git a/dev-infra/commit-message/restore-commit-message/restore-commit-message.ts b/dev-infra/commit-message/restore-commit-message/restore-commit-message.ts deleted file mode 100644 index 2a5d1be929f742..00000000000000 --- a/dev-infra/commit-message/restore-commit-message/restore-commit-message.ts +++ /dev/null @@ -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); -} diff --git a/dev-infra/commit-message/test-util.ts b/dev-infra/commit-message/test-util.ts deleted file mode 100644 index 0371cf0ec32e4d..00000000000000 --- a/dev-infra/commit-message/test-util.ts +++ /dev/null @@ -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 = {}) => { - 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}`; - }; -} diff --git a/dev-infra/commit-message/utils.ts b/dev-infra/commit-message/utils.ts deleted file mode 100644 index 82af851e5d9f27..00000000000000 --- a/dev-infra/commit-message/utils.ts +++ /dev/null @@ -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 { - 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)); - }); -} diff --git a/dev-infra/commit-message/validate-file/cli.ts b/dev-infra/commit-message/validate-file/cli.ts deleted file mode 100644 index 8ba2d5b2856b36..00000000000000 --- a/dev-infra/commit-message/validate-file/cli.ts +++ /dev/null @@ -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) { - 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', -}; diff --git a/dev-infra/commit-message/validate-file/validate-file.ts b/dev-infra/commit-message/validate-file/validate-file.ts deleted file mode 100644 index 31358459ddcfd6..00000000000000 --- a/dev-infra/commit-message/validate-file/validate-file.ts +++ /dev/null @@ -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; -} diff --git a/dev-infra/commit-message/validate-range/cli.ts b/dev-infra/commit-message/validate-range/cli.ts deleted file mode 100644 index 2878dc1b7a1cdf..00000000000000 --- a/dev-infra/commit-message/validate-range/cli.ts +++ /dev/null @@ -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) { - // 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 [ending-ref]', - describe: 'Validate a range of commit messages', -}; diff --git a/dev-infra/commit-message/validate-range/validate-range.ts b/dev-infra/commit-message/validate-range/validate-range.ts deleted file mode 100644 index 5f506b99163239..00000000000000 --- a/dev-infra/commit-message/validate-range/validate-range.ts +++ /dev/null @@ -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); - } -} diff --git a/dev-infra/commit-message/validate.spec.ts b/dev-infra/commit-message/validate.spec.ts deleted file mode 100644 index 0c0b534ea5ddd3..00000000000000 --- a/dev-infra/commit-message/validate.spec.ts +++ /dev/null @@ -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); - }); - - 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 "(): " 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.']); - }); - }); - }); -}); diff --git a/dev-infra/commit-message/validate.ts b/dev-infra/commit-message/validate.ts deleted file mode 100644 index 4a9215d602fec4..00000000000000 --- a/dev-infra/commit-message/validate.ts +++ /dev/null @@ -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 ` | Here we assume the colon is missing by accident. - * - `BREAKING-CHANGE: ` | The wrong keyword is used here. - * - `BREAKING CHANGES: ` | The wrong keyword is used here. - * - `BREAKING-CHANGES: ` | 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 ` | Here we assume the colon is missing by accident. - * - `DEPRECATIONS: ` | The wrong keyword is used here. - * - `DEPRECATE: ` | The wrong keyword is used here. - * - `DEPRECATES: ` | 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('(): '); - print(); - print(''); - print(); - print(`BREAKING CHANGE: `); - print(); - print(``); - print(); - print(); -} diff --git a/dev-infra/defaults.bzl b/dev-infra/defaults.bzl deleted file mode 100644 index b7f4ad2a084765..00000000000000 --- a/dev-infra/defaults.bzl +++ /dev/null @@ -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, - ) diff --git a/dev-infra/format/BUILD.bazel b/dev-infra/format/BUILD.bazel deleted file mode 100644 index 1c5a82ea8576eb..00000000000000 --- a/dev-infra/format/BUILD.bazel +++ /dev/null @@ -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", - ], -) diff --git a/dev-infra/format/cli.ts b/dev-infra/format/cli.ts deleted file mode 100644 index 47261b58aea6d2..00000000000000 --- a/dev-infra/format/cli.ts +++ /dev/null @@ -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 ', 'Run the formatter on provided files', - args => args.positional('files', {array: true, type: 'string'}), ({check, files}) => { - const executionCmd = check ? checkFiles : formatFiles; - executionCmd(files!); - }); -} diff --git a/dev-infra/format/config.ts b/dev-infra/format/config.ts deleted file mode 100644 index 62be7a6ca94f29..00000000000000 --- a/dev-infra/format/config.ts +++ /dev/null @@ -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> = 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; -} - -/** Validate an individual Formatter config. */ -function checkFormatterConfig(key: string, config: Partial, errors: string[]) { - if (config.matchers === undefined) { - errors.push(`Missing "format.${key}.matchers" value`); - } -} diff --git a/dev-infra/format/format.ts b/dev-infra/format/format.ts deleted file mode 100644 index 50e458503dc7c1..00000000000000 --- a/dev-infra/format/format.ts +++ /dev/null @@ -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); - } -} diff --git a/dev-infra/format/formatters/base-formatter.ts b/dev-infra/format/formatters/base-formatter.ts deleted file mode 100644 index 65930adadb9c64..00000000000000 --- a/dev-infra/format/formatters/base-formatter.ts +++ /dev/null @@ -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; - } -} diff --git a/dev-infra/format/formatters/buildifier.ts b/dev-infra/format/formatters/buildifier.ts deleted file mode 100644 index 3f49dc1b3c36c2..00000000000000 --- a/dev-infra/format/formatters/buildifier.ts +++ /dev/null @@ -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`; diff --git a/dev-infra/format/formatters/clang-format.ts b/dev-infra/format/formatters/clang-format.ts deleted file mode 100644 index 616849a5791ca1..00000000000000 --- a/dev-infra/format/formatters/clang-format.ts +++ /dev/null @@ -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 {join} from 'path'; - -import {error} from '../../utils/console'; - -import {Formatter} from './base-formatter'; - -/** - * Formatter for running clang-format against Typescript and Javascript files - */ -export class ClangFormat extends Formatter { - override name = 'clang-format'; - - override binaryFilePath = join(this.git.baseDir, 'node_modules/.bin/clang-format'); - - override defaultFileMatcher = ['**/*.{t,j}s']; - - override actions = { - check: { - commandFlags: `--Werror -n -style=file`, - callback: - (_: string, code: number|NodeJS.Signals) => { - return code !== 0; - }, - }, - format: { - commandFlags: `-i -style=file`, - callback: - (file: string, code: number|NodeJS.Signals, _: string, stderr: string) => { - if (code !== 0) { - error(`Error running clang-format on: ${file}`); - error(stderr); - error(); - return true; - } - return false; - } - } - }; -} diff --git a/dev-infra/format/formatters/index.ts b/dev-infra/format/formatters/index.ts deleted file mode 100644 index 3279c3c279aa66..00000000000000 --- a/dev-infra/format/formatters/index.ts +++ /dev/null @@ -1,28 +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 {getFormatConfig} from '../config'; - -import {Buildifier} from './buildifier'; -import {ClangFormat} from './clang-format'; -import {Prettier} from './prettier'; - -/** - * Get all defined formatters which are active based on the current loaded config. - */ -export function getActiveFormatters() { - const config = getFormatConfig().format; - return [ - new Prettier(config), - new Buildifier(config), - new ClangFormat(config), - ].filter((formatter) => formatter.isEnabled()); -} - -// Rexport symbols used for types elsewhere. -export {Formatter, FormatterAction} from './base-formatter'; diff --git a/dev-infra/format/formatters/prettier.ts b/dev-infra/format/formatters/prettier.ts deleted file mode 100644 index f2969203c76e3a..00000000000000 --- a/dev-infra/format/formatters/prettier.ts +++ /dev/null @@ -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 {join} from 'path'; - -import {spawnSync} from '../../utils/child-process'; -import {error} from '../../utils/console'; - -import {Formatter} from './base-formatter'; - -/** - * Formatter for running prettier against Typescript and Javascript files. - */ -export class Prettier extends Formatter { - override name = 'prettier'; - - override binaryFilePath = join(this.git.baseDir, 'node_modules/.bin/prettier'); - - override defaultFileMatcher = ['**/*.{t,j}s']; - - /** - * The configuration path of the prettier config, obtained during construction to prevent needing - * to discover it repeatedly for each execution. - */ - private configPath = this.config['prettier'] ? - spawnSync(this.binaryFilePath, ['--find-config-path', '.']).stdout.trim() : - ''; - - override actions = { - check: { - commandFlags: `--config ${this.configPath} --check`, - callback: - (_: string, code: number|NodeJS.Signals, stdout: string) => { - return code !== 0; - }, - }, - format: { - commandFlags: `--config ${this.configPath} --write`, - callback: - (file: string, code: number|NodeJS.Signals, _: string, stderr: string) => { - if (code !== 0) { - error(`Error running prettier on: ${file}`); - error(stderr); - error(); - return true; - } - return false; - }, - }, - }; -} diff --git a/dev-infra/format/run-commands-parallel.ts b/dev-infra/format/run-commands-parallel.ts deleted file mode 100644 index c10a27f9c9c77e..00000000000000 --- a/dev-infra/format/run-commands-parallel.ts +++ /dev/null @@ -1,122 +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 {Bar} from 'cli-progress'; -import * as multimatch from 'multimatch'; -import {cpus} from 'os'; - -import {spawn, SpawnResult} from '../utils/child-process'; -import {info} from '../utils/console'; - -import {Formatter, FormatterAction, getActiveFormatters} from './formatters/index'; - -const AVAILABLE_THREADS = Math.max(cpus().length - 1, 1); - -/** Interface describing a failure occurred during formatting of a file. */ -export interface FormatFailure { - /** Path to the file that failed. */ - filePath: string; - /** Error message reported by the formatter. */ - message: string; -} - -/** - * Run the provided commands in parallel for each provided file. - * - * Running the formatter is split across (number of available cpu threads - 1) processess. - * The task is done in multiple processess to speed up the overall time of the task, as running - * across entire repositories takes a large amount of time. - * As a data point for illustration, using 8 process rather than 1 cut the execution - * time from 276 seconds to 39 seconds for the same 2700 files. - * - * A promise is returned, completed when the command has completed running for each file. - * The promise resolves with a list of failures, or `false` if no formatters have matched. - */ -export function runFormatterInParallel(allFiles: string[], action: FormatterAction) { - return new Promise((resolve) => { - const formatters = getActiveFormatters(); - const failures: FormatFailure[] = []; - const pendingCommands: {formatter: Formatter, file: string}[] = []; - - for (const formatter of formatters) { - pendingCommands.push( - ...multimatch.call(undefined, allFiles, formatter.getFileMatcher(), {dot: true}) - .map(file => ({formatter, file}))); - } - - // If no commands are generated, resolve the promise as `false` as no files - // were run against the any formatters. - if (pendingCommands.length === 0) { - return resolve(false); - } - - switch (action) { - case 'format': - info(`Formatting ${pendingCommands.length} file(s)`); - break; - case 'check': - info(`Checking format of ${pendingCommands.length} file(s)`); - break; - default: - throw Error(`Invalid format action "${action}": allowed actions are "format" and "check"`); - } - - // The progress bar instance to use for progress tracking. - const progressBar = - new Bar({format: `[{bar}] ETA: {eta}s | {value}/{total} files`, clearOnComplete: true}); - // A local copy of the files to run the command on. - // An array to represent the current usage state of each of the threads for parallelization. - const threads = new Array(AVAILABLE_THREADS).fill(false); - - // Recursively run the command on the next available file from the list using the provided - // thread. - function runCommandInThread(thread: number) { - const nextCommand = pendingCommands.pop(); - // If no file was pulled from the array, return as there are no more files to run against. - if (nextCommand === undefined) { - threads[thread] = false; - return; - } - - // Get the file and formatter for the next command. - const {file, formatter} = nextCommand; - - const [spawnCmd, ...spawnArgs] = [...formatter.commandFor(action).split(' '), file]; - spawn(spawnCmd, spawnArgs, {suppressErrorOnFailingExitCode: true, mode: 'silent'}) - .then(({stdout, stderr, status}: SpawnResult) => { - // Run the provided callback function. - const failed = formatter.callbackFor(action)(file, status, stdout, stderr); - if (failed) { - failures.push({filePath: file, message: stderr}); - } - // Note in the progress bar another file being completed. - progressBar.increment(1); - // If more files exist in the list, run again to work on the next file, - // using the same slot. - if (pendingCommands.length) { - return runCommandInThread(thread); - } - // If not more files are available, mark the thread as unused. - threads[thread] = false; - // If all of the threads are false, as they are unused, mark the progress bar - // completed and resolve the promise. - if (threads.every(active => !active)) { - progressBar.stop(); - resolve(failures); - } - }); - // Mark the thread as in use as the command execution has been started. - threads[thread] = true; - } - - // Start the progress bar - progressBar.start(pendingCommands.length, 0); - // Start running the command on files from the least in each available thread. - threads.forEach((_, idx) => runCommandInThread(idx)); - }); -} diff --git a/dev-infra/index.bzl b/dev-infra/index.bzl deleted file mode 100644 index cd495e8552cf4b..00000000000000 --- a/dev-infra/index.bzl +++ /dev/null @@ -1,9 +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 - -# File is currently empty but serves 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. diff --git a/dev-infra/misc/BUILD.bazel b/dev-infra/misc/BUILD.bazel deleted file mode 100644 index 49c38e15615038..00000000000000 --- a/dev-infra/misc/BUILD.bazel +++ /dev/null @@ -1,15 +0,0 @@ -load("//dev-infra:defaults.bzl", "ts_library") - -ts_library( - name = "misc", - srcs = glob(["**/*.ts"]), - visibility = ["//dev-infra:__subpackages__"], - deps = [ - "//dev-infra/release/build", - "//dev-infra/release/config", - "//dev-infra/utils", - "@npm//@types/node", - "@npm//@types/yargs", - "@npm//chalk", - ], -) diff --git a/dev-infra/misc/build-and-link/cli.ts b/dev-infra/misc/build-and-link/cli.ts deleted file mode 100644 index d9144b6a321064..00000000000000 --- a/dev-infra/misc/build-and-link/cli.ts +++ /dev/null @@ -1,69 +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 {green} from 'chalk'; -import {lstatSync} from 'fs'; -import {resolve} from 'path'; -import {Arguments, Argv, CommandModule} from 'yargs'; - -import {buildReleaseOutput} from '../../release/build/index'; -import {spawn} from '../../utils/child-process'; -import {error, info, red} from '../../utils/console'; - - -/** Command line options. */ -export interface BuildAndLinkOptions { - projectRoot: string; -} - -/** Yargs command builder for the command. */ -function builder(argv: Argv): Argv { - return argv.positional('projectRoot', { - type: 'string', - normalize: true, - coerce: (path: string) => resolve(path), - demandOption: true, - }); -} - -/** Yargs command handler for the command. */ -async function handler({projectRoot}: Arguments) { - try { - if (!lstatSync(projectRoot).isDirectory()) { - error(red(` ✘ The 'projectRoot' must be a directory: ${projectRoot}`)); - process.exit(1); - } - } catch { - error(red(` ✘ Could not find the 'projectRoot' provided: ${projectRoot}`)); - process.exit(1); - } - - const releaseOutputs = await buildReleaseOutput(false); - - if (releaseOutputs === null) { - error(red(` ✘ Could not build release output. Please check output above.`)); - process.exit(1); - } - info(green(` ✓ Built release output.`)); - - for (const {outputPath, name} of releaseOutputs) { - await spawn('yarn', ['link', '--cwd', outputPath]); - await spawn('yarn', ['link', '--cwd', projectRoot, name]); - } - - info(green(` ✓ Linked release packages in provided project.`)); -} - -/** CLI command module. */ -export const BuildAndLinkCommandModule: CommandModule<{}, BuildAndLinkOptions> = { - builder, - handler, - command: 'build-and-link ', - describe: - 'Builds the release output, registers the outputs as linked, and links via yarn to the provided project', -}; diff --git a/dev-infra/misc/cli.ts b/dev-infra/misc/cli.ts deleted file mode 100644 index 64b2b785a4f612..00000000000000 --- a/dev-infra/misc/cli.ts +++ /dev/null @@ -1,15 +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 {BuildAndLinkCommandModule} from './build-and-link/cli'; - -/** Build the parser for the misc commands. */ -export function buildMiscParser(localYargs: yargs.Argv) { - return localYargs.help().strict().command(BuildAndLinkCommandModule); -} diff --git a/dev-infra/ng-dev.js b/dev-infra/ng-dev.js deleted file mode 100755 index 32da46f99f20a7..00000000000000 --- a/dev-infra/ng-dev.js +++ /dev/null @@ -1,8340 +0,0 @@ -#!/usr/bin/env node -'use strict'; - -function _interopDefault (ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; } - -var yargs = require('yargs'); -var tslib = require('tslib'); -var chalk = require('chalk'); -var fs = require('fs'); -var inquirer = require('inquirer'); -var path = require('path'); -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'); -var fetch = _interopDefault(require('node-fetch')); -var multimatch = require('multimatch'); -var yaml = require('yaml'); -var conventionalCommitsParser = require('conventional-commits-parser'); -var gitCommits_ = require('git-raw-commits'); -var cliProgress = require('cli-progress'); -var os = require('os'); -var minimatch = require('minimatch'); -var ejs = require('ejs'); -var ora = require('ora'); -var glob = require('glob'); -var ts = require('typescript'); - -/** - * @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; -/** - * The filename expected for local user config, without the file extension to allow a typescript, - * javascript or json file to be used. - */ -var USER_CONFIG_FILE_PATH = '.ng-dev.user'; -/** The local user configuration for ng-dev. */ -var userConfig = 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); -} -/** - * Get the local user configuration from the file system, returning the already loaded copy if it is - * defined. - * - * @returns The user configuration object, or an empty object if no user configuration file is - * present. The object is an untyped object as there are no required user configurations. - */ -function getUserConfig() { - // If the global config is not defined, load it from the file system. - if (userConfig === null) { - var git = GitClient.get(); - // The full path to the configuration file. - var configPath = path.join(git.baseDir, USER_CONFIG_FILE_PATH); - // Set the global config object. - userConfig = readConfigFile(configPath, true); - } - // Return a clone of the user 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({}, userConfig); -} - -/** - * @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 - */ -/** URL to the Github page where personal access tokens can be managed. */ -var GITHUB_TOKEN_SETTINGS_URL = 'https://github.com/settings/tokens'; -/** URL to the Github page where personal access tokens can be generated. */ -var GITHUB_TOKEN_GENERATE_URL = 'https://github.com/settings/tokens/new'; -/** 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; -} -/** Gets a Github URL that refers to a list of recent commits within a specified branch. */ -function getListCommitsInBranchUrl(_a, branchName) { - var remoteParams = _a.remoteParams; - return "https://github.com/" + remoteParams.owner + "/" + remoteParams.repo + "/commits/" + branchName; -} - -/** - * @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 - */ -/** Reexport of chalk colors for convenient access. */ -var red = chalk.red; -var green = chalk.green; -var yellow = chalk.yellow; -var bold = chalk.bold; -var blue = chalk.blue; -/** Prompts the user with a confirmation question and a specified message. */ -function promptConfirm(message, defaultValue) { - if (defaultValue === void 0) { defaultValue = false; } - return tslib.__awaiter(this, void 0, void 0, function () { - return tslib.__generator(this, function (_a) { - switch (_a.label) { - case 0: return [4 /*yield*/, inquirer.prompt({ - type: 'confirm', - name: 'result', - message: message, - default: defaultValue, - })]; - case 1: return [2 /*return*/, (_a.sent()) - .result]; - } - }); - }); -} -/** Prompts the user for one line of input. */ -function promptInput(message) { - return tslib.__awaiter(this, void 0, void 0, function () { - return tslib.__generator(this, function (_a) { - switch (_a.label) { - case 0: return [4 /*yield*/, inquirer.prompt({ type: 'input', name: 'result', message: message })]; - case 1: return [2 /*return*/, (_a.sent()).result]; - } - }); - }); -} -/** - * 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; -} -/** All text to write to the log file. */ -var LOGGED_TEXT = ''; -/** Whether file logging as been enabled. */ -var FILE_LOGGING_ENABLED = false; -/** - * The number of columns used in the prepended log level information on each line of the logging - * output file. - */ -var LOG_LEVEL_COLUMNS = 7; -/** - * Enable writing the logged outputs to the log file on process exit, sets initial lines from the - * command execution, containing information about the timing and command parameters. - * - * This is expected to be called only once during a command run, and should be called by the - * middleware of yargs to enable the file logging before the rest of the command parsing and - * response is executed. - */ -function captureLogOutputForCommand(argv) { - if (FILE_LOGGING_ENABLED) { - throw Error('`captureLogOutputForCommand` cannot be called multiple times'); - } - var git = GitClient.get(); - /** The date time used for timestamping when the command was invoked. */ - var now = new Date(); - /** Header line to separate command runs in log files. */ - var headerLine = Array(100).fill('#').join(''); - LOGGED_TEXT += headerLine + "\nCommand: " + argv.$0 + " " + argv._.join(' ') + "\nRan at: " + now + "\n"; - // On process exit, write the logged output to the appropriate log files - process.on('exit', function (code) { - LOGGED_TEXT += headerLine + "\n"; - LOGGED_TEXT += "Command ran in " + (new Date().getTime() - now.getTime()) + "ms\n"; - LOGGED_TEXT += "Exit Code: " + code + "\n"; - /** Path to the log file location. */ - var logFilePath = path.join(git.baseDir, '.ng-dev.log'); - // Strip ANSI escape codes from log outputs. - LOGGED_TEXT = LOGGED_TEXT.replace(/\x1B\[([0-9]{1,3}(;[0-9]{1,2})?)?[mGK]/g, ''); - fs.writeFileSync(logFilePath, LOGGED_TEXT); - // For failure codes greater than 1, the new logged lines should be written to a specific log - // file for the command run failure. - if (code > 1) { - var logFileName = ".ng-dev.err-" + now.getTime() + ".log"; - console.error("Exit code: " + code + ". Writing full log to " + logFileName); - fs.writeFileSync(path.join(git.baseDir, logFileName), LOGGED_TEXT); - } - }); - // Mark file logging as enabled to prevent the function from executing multiple times. - FILE_LOGGING_ENABLED = true; -} -/** 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); - LOGGED_TEXT += text.join(' ').split('\n').map(function (l) { return logLevelText + " " + l + "\n"; }).join(''); -} - -/** - * @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 - */ -/** - * Extension of the `GitClient` with additional utilities which are useful for - * authenticated Git client instances. - */ -var AuthenticatedGitClient = /** @class */ (function (_super) { - tslib.__extends(AuthenticatedGitClient, _super); - function AuthenticatedGitClient(githubToken, baseDir, config) { - var _this = _super.call(this, baseDir, config) || this; - _this.githubToken = githubToken; - /** - * Regular expression that matches the provided Github token. Used for - * sanitizing the token from Git child process output. - */ - _this._githubTokenRegex = new RegExp(_this.githubToken, 'g'); - /** The OAuth scopes available for the provided Github token. */ - _this._cachedOauthScopes = null; - /** Instance of an authenticated github client. */ - _this.github = new AuthenticatedGithubClient(_this.githubToken); - return _this; - } - /** Sanitizes a given message by omitting the provided Github token if present. */ - AuthenticatedGitClient.prototype.sanitizeConsoleOutput = function (value) { - return value.replace(this._githubTokenRegex, ''); - }; - /** Git URL that resolves to the configured repository. */ - AuthenticatedGitClient.prototype.getRepoGitUrl = function () { - return getRepositoryGitUrl(this.remoteConfig, this.githubToken); - }; - /** - * Assert the GitClient instance is using a token with permissions for the all of the - * provided OAuth scopes. - */ - AuthenticatedGitClient.prototype.hasOauthScopes = function (testFn) { - return tslib.__awaiter(this, void 0, void 0, function () { - var scopes, missingScopes, error; - return tslib.__generator(this, function (_a) { - switch (_a.label) { - case 0: return [4 /*yield*/, this._fetchAuthScopesForToken()]; - case 1: - scopes = _a.sent(); - missingScopes = []; - // Test Github OAuth scopes and collect missing ones. - testFn(scopes, missingScopes); - // If no missing scopes are found, return true to indicate all OAuth Scopes are available. - if (missingScopes.length === 0) { - return [2 /*return*/, true]; - } - error = "The provided does not have required permissions due to missing scope(s): " + - (yellow(missingScopes.join(', ')) + "\n\n") + - "Update the token in use at:\n" + - (" " + GITHUB_TOKEN_SETTINGS_URL + "\n\n") + - ("Alternatively, a new token can be created at: " + GITHUB_TOKEN_GENERATE_URL + "\n"); - return [2 /*return*/, { error: error }]; - } - }); - }); - }; - /** Fetch the OAuth scopes for the loaded Github token. */ - AuthenticatedGitClient.prototype._fetchAuthScopesForToken = function () { - // If the OAuth scopes have already been loaded, return the Promise containing them. - if (this._cachedOauthScopes !== null) { - return this._cachedOauthScopes; - } - // OAuth scopes are loaded via the /rate_limit endpoint to prevent - // usage of a request against that rate_limit for this lookup. - return this._cachedOauthScopes = this.github.rateLimit.get().then(function (response) { - var scopes = response.headers['x-oauth-scopes']; - // If no token is provided, or if the Github client is authenticated incorrectly, - // the `x-oauth-scopes` response header is not set. We error in such cases as it - // signifies a faulty of the - if (scopes === undefined) { - throw Error('Unable to retrieve OAuth scopes for token provided to Git client.'); - } - return scopes.split(',').map(function (scope) { return scope.trim(); }).filter(function (scope) { return scope !== ''; }); - }); - }; - /** - * Static method to get the singleton instance of the `AuthenticatedGitClient`, - * creating it if it has not yet been created. - */ - AuthenticatedGitClient.get = function () { - if (!AuthenticatedGitClient._authenticatedInstance) { - throw new Error('No instance of `AuthenticatedGitClient` has been set up yet.'); - } - return AuthenticatedGitClient._authenticatedInstance; - }; - /** Configures an authenticated git client. */ - AuthenticatedGitClient.configure = function (token) { - if (AuthenticatedGitClient._authenticatedInstance) { - throw Error('Unable to configure `AuthenticatedGitClient` as it has been configured already.'); - } - AuthenticatedGitClient._authenticatedInstance = new AuthenticatedGitClient(token); - }; - return AuthenticatedGitClient; -}(GitClient)); - -/** - * @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 - */ -/** Sets up the `github-token` command option for the given Yargs instance. */ -function addGithubTokenOption(yargs) { - return yargs - // 'github-token' is casted to 'githubToken' to properly set up typings to reflect the key in - // the Argv object being camelCase rather than kebab case due to the `camel-case-expansion` - // config: https://github.com/yargs/yargs-parser#camel-case-expansion - .option('github-token', { - type: 'string', - description: 'Github token. If not set, token is retrieved from the environment variables.', - coerce: function (token) { - var githubToken = token || process.env.GITHUB_TOKEN || process.env.TOKEN; - if (!githubToken) { - error(red('No Github token set. Please set the `GITHUB_TOKEN` environment variable.')); - error(red('Alternatively, pass the `--github-token` command line flag.')); - error(yellow("You can generate a token here: " + GITHUB_TOKEN_GENERATE_URL)); - process.exit(1); - } - try { - AuthenticatedGitClient.get(); - } - catch (_a) { - AuthenticatedGitClient.configure(githubToken); - } - return githubToken; - }, - }) - .default('github-token', '', ''); -} - -/** - * @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 `CaretakerConfig`. */ -function getCaretakerConfig() { - // List of errors encountered validating the config. - const errors = []; - // The non-validated config object. - const config = getConfig(); - assertNoErrors(errors); - return config; -} - -/** - * @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 - */ -/** Class describing a release-train. */ -class ReleaseTrain { - constructor( - /** Name of the branch for this release-train. */ - branchName, - /** Most recent version for this release train. */ - version) { - this.branchName = branchName; - this.version = version; - /** Whether the release train is currently targeting a major. */ - this.isMajor = this.version.minor === 0 && this.version.patch === 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 - */ -/** Regular expression that matches version-branches. */ -const versionBranchNameRegex = /^(\d+)\.(\d+)\.x$/; -/** Gets the version of a given branch by reading the `package.json` upstream. */ -function getVersionOfBranch(repo, branchName) { - return tslib.__awaiter(this, void 0, void 0, function* () { - const { data } = yield repo.api.repos.getContent({ owner: repo.owner, repo: repo.name, path: '/package.json', ref: branchName }); - // Workaround for: https://github.com/octokit/rest.js/issues/32. - // TODO: Remove cast once types of Octokit `getContent` are fixed. - const content = data.content; - if (!content) { - throw Error(`Unable to read "package.json" file from repository.`); - } - const { version } = JSON.parse(Buffer.from(content, 'base64').toString()); - const parsedVersion = semver.parse(version); - if (parsedVersion === null) { - throw Error(`Invalid version detected in following branch: ${branchName}.`); - } - return parsedVersion; - }); -} -/** Whether the given branch corresponds to a version branch. */ -function isVersionBranch(branchName) { - return versionBranchNameRegex.test(branchName); -} -/** - * Converts a given version-branch into a SemVer version that can be used with SemVer - * utilities. e.g. to determine semantic order, extract major digit, compare. - * - * For example `10.0.x` will become `10.0.0` in SemVer. The patch digit is not - * relevant but needed for parsing. SemVer does not allow `x` as patch digit. - */ -function getVersionForVersionBranch(branchName) { - return semver.parse(branchName.replace(versionBranchNameRegex, '$1.$2.0')); -} -/** - * Gets the version branches for the specified major versions in descending - * order. i.e. latest version branches first. - */ -function getBranchesForMajorVersions(repo, majorVersions) { - return tslib.__awaiter(this, void 0, void 0, function* () { - const branchData = yield repo.api.paginate(repo.api.repos.listBranches, { owner: repo.owner, repo: repo.name, protected: true }); - const branches = []; - for (const { name } of branchData) { - if (!isVersionBranch(name)) { - continue; - } - // Convert the version-branch into a SemVer version that can be used with the - // SemVer utilities. e.g. to determine semantic order, compare versions. - const parsed = getVersionForVersionBranch(name); - // Collect all version-branches that match the specified major versions. - if (parsed !== null && majorVersions.includes(parsed.major)) { - branches.push({ name, parsed }); - } - } - // Sort captured version-branches in descending order. - return branches.sort((a, b) => semver.rcompare(a.parsed, b.parsed)); - }); -} - -/** - * @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 - */ -/** Branch name for the `next` branch. */ -const nextBranchName = 'master'; -/** Fetches the active release trains for the configured project. */ -function fetchActiveReleaseTrains(repo) { - return tslib.__awaiter(this, void 0, void 0, function* () { - const nextVersion = yield getVersionOfBranch(repo, nextBranchName); - const next = new ReleaseTrain(nextBranchName, nextVersion); - const majorVersionsToConsider = []; - let expectedReleaseCandidateMajor; - // If the `next` branch (i.e. `master` branch) is for an upcoming major version, we know - // that there is no patch branch or feature-freeze/release-candidate branch for this major - // digit. If the current `next` version is the first minor of a major version, we know that - // the feature-freeze/release-candidate branch can only be the actual major branch. The - // patch branch is based on that, either the actual major branch or the last minor from the - // preceding major version. In all other cases, the patch branch and feature-freeze or - // release-candidate branch are part of the same major version. Consider the following: - // - // CASE 1. next: 11.0.0-next.0: patch and feature-freeze/release-candidate can only be - // most recent `10.<>.x` branches. The FF/RC branch can only be the last-minor of v10. - // CASE 2. next: 11.1.0-next.0: patch can be either `11.0.x` or last-minor in v10 based - // on whether there is a feature-freeze/release-candidate branch (=> `11.0.x`). - // CASE 3. next: 10.6.0-next.0: patch can be either `10.5.x` or `10.4.x` based on whether - // there is a feature-freeze/release-candidate branch (=> `10.5.x`) - if (nextVersion.minor === 0) { - expectedReleaseCandidateMajor = nextVersion.major - 1; - majorVersionsToConsider.push(nextVersion.major - 1); - } - else if (nextVersion.minor === 1) { - expectedReleaseCandidateMajor = nextVersion.major; - majorVersionsToConsider.push(nextVersion.major, nextVersion.major - 1); - } - else { - expectedReleaseCandidateMajor = nextVersion.major; - majorVersionsToConsider.push(nextVersion.major); - } - // Collect all version-branches that should be considered for the latest version-branch, - // or the feature-freeze/release-candidate. - const branches = yield getBranchesForMajorVersions(repo, majorVersionsToConsider); - const { latest, releaseCandidate } = yield findActiveReleaseTrainsFromVersionBranches(repo, nextVersion, branches, expectedReleaseCandidateMajor); - if (latest === null) { - throw Error(`Unable to determine the latest release-train. The following branches ` + - `have been considered: [${branches.map(b => b.name).join(', ')}]`); - } - return { releaseCandidate, latest, next }; - }); -} -/** Finds the currently active release trains from the specified version branches. */ -function findActiveReleaseTrainsFromVersionBranches(repo, nextVersion, branches, expectedReleaseCandidateMajor) { - return tslib.__awaiter(this, void 0, void 0, function* () { - // Version representing the release-train currently in the next phase. Note that we ignore - // patch and pre-release segments in order to be able to compare the next release train to - // other release trains from version branches (which follow the `N.N.x` pattern). - const nextReleaseTrainVersion = semver.parse(`${nextVersion.major}.${nextVersion.minor}.0`); - let latest = null; - let releaseCandidate = null; - // Iterate through the captured branches and find the latest non-prerelease branch and a - // potential release candidate branch. From the collected branches we iterate descending - // order (most recent semantic version-branch first). The first branch is either the latest - // active version branch (i.e. patch) or a feature-freeze/release-candidate branch. A FF/RC - // branch cannot be older than the latest active version-branch, so we stop iterating once - // we found such a branch. Otherwise, if we found a FF/RC branch, we continue looking for the - // next version-branch as that one is supposed to be the latest active version-branch. If it - // is not, then an error will be thrown due to two FF/RC branches existing at the same time. - for (const { name, parsed } of branches) { - // It can happen that version branches have been accidentally created which are more recent - // than the release-train in the next branch (i.e. `master`). We could ignore such branches - // silently, but it might be symptomatic for an outdated version in the `next` branch, or an - // accidentally created branch by the caretaker. In either way we want to raise awareness. - if (semver.gt(parsed, nextReleaseTrainVersion)) { - throw Error(`Discovered unexpected version-branch "${name}" for a release-train that is ` + - `more recent than the release-train currently in the "${nextBranchName}" branch. ` + - `Please either delete the branch if created by accident, or update the outdated ` + - `version in the next branch (${nextBranchName}).`); - } - else if (semver.eq(parsed, nextReleaseTrainVersion)) { - throw Error(`Discovered unexpected version-branch "${name}" for a release-train that is already ` + - `active in the "${nextBranchName}" branch. Please either delete the branch if ` + - `created by accident, or update the version in the next branch (${nextBranchName}).`); - } - const version = yield getVersionOfBranch(repo, name); - const releaseTrain = new ReleaseTrain(name, version); - const isPrerelease = version.prerelease[0] === 'rc' || version.prerelease[0] === 'next'; - if (isPrerelease) { - if (releaseCandidate !== null) { - throw Error(`Unable to determine latest release-train. Found two consecutive ` + - `branches in feature-freeze/release-candidate phase. Did not expect both "${name}" ` + - `and "${releaseCandidate.branchName}" to be in feature-freeze/release-candidate mode.`); - } - else if (version.major !== expectedReleaseCandidateMajor) { - throw Error(`Discovered unexpected old feature-freeze/release-candidate branch. Expected no ` + - `version-branch in feature-freeze/release-candidate mode for v${version.major}.`); - } - releaseCandidate = releaseTrain; - } - else { - latest = releaseTrain; - break; - } - } - return { releaseCandidate, latest }; - }); -} - -/** - * @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 - */ -/** - * Cache for requested NPM package information. A cache is desirable as the NPM - * registry requests are usually very large and slow. - */ -const _npmPackageInfoCache = {}; -/** - * Fetches the NPM package representing the project. Angular repositories usually contain - * multiple packages in a monorepo scheme, but packages dealt with as part of the release - * tooling are released together with the same versioning and branching. This means that - * a single package can be used as source of truth for NPM package queries. - */ -function fetchProjectNpmPackageInfo(config) { - return tslib.__awaiter(this, void 0, void 0, function* () { - const pkgName = getRepresentativeNpmPackage(config); - return yield fetchPackageInfoFromNpmRegistry(pkgName); - }); -} -/** Gets whether the given version is published to NPM or not */ -function isVersionPublishedToNpm(version, config) { - return tslib.__awaiter(this, void 0, void 0, function* () { - const { versions } = yield fetchProjectNpmPackageInfo(config); - return versions[version.format()] !== undefined; - }); -} -/** - * Gets the representative NPM package for the specified release configuration. Angular - * repositories usually contain multiple packages in a monorepo scheme, but packages dealt with - * as part of the release tooling are released together with the same versioning and branching. - * This means that a single package can be used as source of truth for NPM package queries. - */ -function getRepresentativeNpmPackage(config) { - return config.npmPackages[0]; -} -/** Fetches the specified NPM package from the NPM registry. */ -function fetchPackageInfoFromNpmRegistry(pkgName) { - return tslib.__awaiter(this, void 0, void 0, function* () { - if (_npmPackageInfoCache[pkgName] === undefined) { - _npmPackageInfoCache[pkgName] = - fetch(`https://registry.npmjs.org/${pkgName}`).then(r => r.json()); - } - return yield _npmPackageInfoCache[pkgName]; - }); -} - -/** - * @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 - */ -/** - * Number of months a major version in Angular is actively supported. See: - * https://angular.io/guide/releases#support-policy-and-schedule. - */ -const majorActiveSupportDuration = 6; -/** - * Number of months a major version has active long-term support. See: - * https://angular.io/guide/releases#support-policy-and-schedule. - */ -const majorLongTermSupportDuration = 12; -/** Regular expression that matches LTS NPM dist tags. */ -const ltsNpmDistTagRegex = /^v(\d+)-lts$/; -/** Finds all long-term support release trains from the specified NPM package. */ -function fetchLongTermSupportBranchesFromNpm(config) { - return tslib.__awaiter(this, void 0, void 0, function* () { - const { 'dist-tags': distTags, time } = yield fetchProjectNpmPackageInfo(config); - const today = new Date(); - const active = []; - const inactive = []; - // Iterate through the NPM package information and determine active/inactive LTS versions with - // their corresponding branches. We assume that an LTS tagged version in NPM belongs to the - // last-minor branch of a given major (i.e. we assume there are no outdated LTS NPM dist tags). - for (const npmDistTag in distTags) { - if (isLtsDistTag(npmDistTag)) { - const version = semver.parse(distTags[npmDistTag]); - const branchName = `${version.major}.${version.minor}.x`; - const majorReleaseDate = new Date(time[`${version.major}.0.0`]); - const ltsEndDate = computeLtsEndDateOfMajor(majorReleaseDate); - const ltsBranch = { name: branchName, version, npmDistTag }; - // Depending on whether the LTS phase is still active, add the branch - // to the list of active or inactive LTS branches. - if (today <= ltsEndDate) { - active.push(ltsBranch); - } - else { - inactive.push(ltsBranch); - } - } - } - // Sort LTS branches in descending order. i.e. most recent ones first. - active.sort((a, b) => semver.rcompare(a.version, b.version)); - inactive.sort((a, b) => semver.rcompare(a.version, b.version)); - return { active, inactive }; - }); -} -/** Gets whether the specified tag corresponds to a LTS dist tag. */ -function isLtsDistTag(tagName) { - return ltsNpmDistTagRegex.test(tagName); -} -/** - * Computes the date when long-term support ends for a major released at the - * specified date. - */ -function computeLtsEndDateOfMajor(majorReleaseDate) { - return new Date(majorReleaseDate.getFullYear(), majorReleaseDate.getMonth() + majorActiveSupportDuration + majorLongTermSupportDuration, majorReleaseDate.getDate(), majorReleaseDate.getHours(), majorReleaseDate.getMinutes(), majorReleaseDate.getSeconds(), majorReleaseDate.getMilliseconds()); -} -/** Gets the long-term support NPM dist tag for a given major version. */ -function getLtsNpmDistTagOfMajor(major) { - // LTS versions should be tagged in NPM in the following format: `v{major}-lts`. - return `v${major}-lts`; -} - -/** - * @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 BaseModule to extend modules for caretaker checks from. */ -class BaseModule { - constructor(config) { - this.config = config; - /** The singleton instance of the authenticated git client. */ - this.git = AuthenticatedGitClient.get(); - /** The data for the module. */ - this.data = this.retrieveData(); - } -} - -/** - * @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 - */ -class CiModule extends BaseModule { - retrieveData() { - return tslib.__awaiter(this, void 0, void 0, function* () { - const gitRepoWithApi = Object.assign({ api: this.git.github }, this.git.remoteConfig); - const releaseTrains = yield fetchActiveReleaseTrains(gitRepoWithApi); - const ciResultPromises = Object.entries(releaseTrains).map(([trainName, train]) => tslib.__awaiter(this, void 0, void 0, function* () { - if (train === null) { - return { - active: false, - name: trainName, - label: '', - status: 'not found', - }; - } - return { - active: true, - name: train.branchName, - label: `${trainName} (${train.branchName})`, - status: yield this.getBranchStatusFromCi(train.branchName), - }; - })); - return yield Promise.all(ciResultPromises); - }); - } - printToTerminal() { - return tslib.__awaiter(this, void 0, void 0, function* () { - const data = yield 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. */ - getBranchStatusFromCi(branch) { - return tslib.__awaiter(this, void 0, void 0, function* () { - const { owner, name } = this.git.remoteConfig; - const url = `https://circleci.com/gh/${owner}/${name}/tree/${branch}.svg?style=shield`; - const result = yield fetch(url).then(result => result.text()); - if (result && !result.includes('no builds')) { - return result.includes('passing') ? 'success' : 'failed'; - } - return 'not found'; - }); - } -} - -/** - * @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 - */ -class G3Module extends BaseModule { - retrieveData() { - return tslib.__awaiter(this, void 0, void 0, function* () { - 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); - }); - } - printToTerminal() { - return tslib.__awaiter(this, void 0, void 0, function* () { - const stats = yield 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. */ - getShaForBranchLatest(branch) { - 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. - */ - getDiffStats(g3Ref, masterRef, includeFiles, excludeFiles) { - /** 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]]) - // 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. */ - checkMatchAgainstIncludeAndExclude(file, includes, excludes) { - return (multimatch.call(undefined, file, includes).length >= 1 && - multimatch.call(undefined, file, excludes).length === 0); - } - getG3FileIncludeAndExcludeLists() { - var _a, _b, _c, _d; - const angularRobotFilePath = path.join(this.git.baseDir, '.github/angular-robot.yml'); - if (!fs.existsSync(angularRobotFilePath)) { - debug('No angular robot configuration file exists, skipping.'); - return null; - } - /** The configuration defined for the angular robot. */ - const robotConfig = yaml.parse(fs.readFileSync(angularRobotFilePath).toString()); - /** The files to be included in the g3 sync. */ - const include = ((_b = (_a = robotConfig === null || robotConfig === void 0 ? void 0 : robotConfig.merge) === null || _a === void 0 ? void 0 : _a.g3Status) === null || _b === void 0 ? void 0 : _b.include) || []; - /** The files to be expected in the g3 sync. */ - const exclude = ((_d = (_c = robotConfig === null || robotConfig === void 0 ? void 0 : robotConfig.merge) === null || _c === void 0 ? void 0 : _c.g3Status) === null || _d === void 0 ? void 0 : _d.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 }; - } - 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 }; - } -} - -/** - * @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 fragment for a result from Github's api for a Github query. */ -const GithubQueryResultFragment = { - issueCount: typedGraphqlify.types.number, - nodes: [Object.assign({}, typedGraphqlify.onUnion({ - PullRequest: { - url: typedGraphqlify.types.string, - }, - Issue: { - url: typedGraphqlify.types.string, - }, - }))], -}; -/** - * 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; -class GithubQueriesModule extends BaseModule { - retrieveData() { - var _a; - return tslib.__awaiter(this, void 0, void 0, function* () { - // 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 = (_a = this.config.caretaker) === null || _a === void 0 ? void 0 : _a.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 = yield 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. */ - buildGraphqlQuery(queries) { - /** The query object for graphql. */ - const graphqlQuery = {}; - 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 = typedGraphqlify.alias(name.replace(/ /g, ''), 'search'); - graphqlQuery[queryKey] = typedGraphqlify.params({ - type: 'ISSUE', - first: MAX_RETURNED_ISSUES, - query: `"${repoFilter} ${query.replace(/"/g, '\\"')}"`, - }, Object.assign({}, GithubQueryResultFragment)); - }); - return graphqlQuery; - } - printToTerminal() { - return tslib.__awaiter(this, void 0, void 0, function* () { - const queryResults = yield 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(); - }); - } -} - -/** - * @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 - */ -/** List of services Angular relies on. */ -const services = [ - { - 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', - }, -]; -class ServicesModule extends BaseModule { - retrieveData() { - return tslib.__awaiter(this, void 0, void 0, function* () { - return Promise.all(services.map(service => this.getStatusFromStandardApi(service))); - }); - } - printToTerminal() { - return tslib.__awaiter(this, void 0, void 0, function* () { - const statuses = yield 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. */ - getStatusFromStandardApi(service) { - return tslib.__awaiter(this, void 0, void 0, function* () { - const result = yield 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) - }; - }); - } -} - -/** - * @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 - */ -/** 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. */ -function checkServiceStatuses() { - return tslib.__awaiter(this, void 0, void 0, function* () { - /** 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 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. - yield Promise.all(caretakerCheckModules.map(module => module.data)); - for (const module of caretakerCheckModules) { - yield module.printToTerminal(); - } - }); -} - -/** - * @license - * Copyright Google LLC All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ -/** Builds the command. */ -function builder(yargs) { - return addGithubTokenOption(yargs); -} -/** Handles the command. */ -function handler() { - return tslib.__awaiter(this, void 0, void 0, function* () { - yield checkServiceStatuses(); - }); -} -/** yargs command module for checking status information for the repository */ -const CheckModule = { - handler, - builder, - command: 'check', - describe: 'Check the status of information the caretaker manages for the repository', -}; - -/** - * @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 - */ -/** Update the Github caretaker group, using a prompt to obtain the new caretaker group members. */ -function updateCaretakerTeamViaPrompt() { - return tslib.__awaiter(this, void 0, void 0, function* () { - /** 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 = yield getGroupMembers(caretakerConfig.caretakerGroup); - /** The list of members able to be added to the group as defined by a separate roster group. */ - const roster = yield 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 } = yield inquirer.prompt([ - { - type: 'checkbox', - choices: roster, - message: 'Select 2 caretakers for the upcoming rotation:', - default: current, - name: 'selected', - prefix: '', - validate: (selected) => { - 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 { - yield setCaretakerGroup(caretakerConfig.caretakerGroup, selected); - } - catch (_a) { - info(red(' ✘ Failed to update caretaker group.')); - return; - } - info(green(' √ Successfully updated caretaker group')); - }); -} -/** Retrieve the current list of members for the provided group. */ -function getGroupMembers(group) { - return tslib.__awaiter(this, void 0, void 0, function* () { - /** The authenticated GitClient instance. */ - const git = AuthenticatedGitClient.get(); - return (yield git.github.teams.listMembersInOrg({ - org: git.remoteConfig.owner, - team_slug: group, - })) - .data.filter(_ => !!_) - .map(member => member.login); - }); -} -function setCaretakerGroup(group, members) { - return tslib.__awaiter(this, void 0, void 0, function* () { - /** The authenticated GitClient instance. */ - const git = AuthenticatedGitClient.get(); - /** The full name of the group /. */ - const fullSlug = `${git.remoteConfig.owner}/${group}`; - /** The list of current members of the group. */ - const current = yield 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 = (username) => tslib.__awaiter(this, void 0, void 0, function* () { - debug(`Adding ${username} to ${fullSlug}.`); - yield git.github.teams.addOrUpdateMembershipForUserInOrg({ - org: git.remoteConfig.owner, - team_slug: group, - username, - role: 'maintainer', - }); - }); - /** Remove a user from the group. */ - const remove = (username) => tslib.__awaiter(this, void 0, void 0, function* () { - debug(`Removing ${username} from ${fullSlug}.`); - yield 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. - yield Promise.all(members.map(add)); - yield Promise.all(removed.map(remove)); - debug(`Successfuly updated ${fullSlug}`); - }); -} - -/** - * @license - * Copyright Google LLC All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ -/** Builds the command. */ -function builder$1(yargs) { - return addGithubTokenOption(yargs); -} -/** Handles the command. */ -function handler$1() { - return tslib.__awaiter(this, void 0, void 0, function* () { - yield updateCaretakerTeamViaPrompt(); - }); -} -/** yargs command module for assisting in handing off caretaker. */ -const HandoffModule = { - handler: handler$1, - builder: builder$1, - command: 'handoff', - describe: 'Run a handoff assistant to aide in moving to the next caretaker', -}; - -/** - * @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 - */ -/** Build the parser for the caretaker commands. */ -function buildCaretakerParser(yargs) { - return yargs.command(CheckModule).command(HandoffModule); -} - -/** - * @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 - */ -/** Load the commit message draft from the file system if it exists. */ -function loadCommitMessageDraft(basePath) { - const commitMessageDraftPath = `${basePath}.ngDevSave`; - if (fs.existsSync(commitMessageDraftPath)) { - return fs.readFileSync(commitMessageDraftPath).toString(); - } - return ''; -} -/** Remove the commit message draft from the file system. */ -function deleteCommitMessageDraft(basePath) { - const commitMessageDraftPath = `${basePath}.ngDevSave`; - if (fs.existsSync(commitMessageDraftPath)) { - fs.unlinkSync(commitMessageDraftPath); - } -} -/** Save the commit message draft to the file system for later retrieval. */ -function saveCommitMessageDraft(basePath, commitMessage) { - fs.writeFileSync(`${basePath}.ngDevSave`, commitMessage); -} - -/** - * @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 - */ -/** - * 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 - */ -function restoreCommitMessage(filePath, source) { - 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) { - fs.writeFileSync(filePath, commitMessage); - } - // Exit the process - process.exit(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 - */ -/** Builds the command. */ -function builder$2(yargs) { - return yargs - .option('file-env-variable', { - 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. */ -function handler$2({ fileEnvVariable, file, source }) { - return tslib.__awaiter(this, void 0, void 0, function* () { - // File and source are provided as command line parameters - if (file !== undefined) { - restoreCommitMessage(file, source); - 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); - 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. */ -const RestoreCommitMessageModule = { - handler: handler$2, - builder: builder$2, - 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, -}; - -/** - * @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 `CommitMessageConfig`. */ -function getCommitMessageConfig() { - // List of errors encountered validating the config. - const errors = []; - // The non-validated config object. - const config = getConfig(); - if (config.commitMessage === undefined) { - errors.push(`No configuration defined for "commitMessage"`); - } - assertNoErrors(errors); - return config; -} -/** Scope requirement level to be set for each commit type. */ -var ScopeRequirement; -(function (ScopeRequirement) { - ScopeRequirement[ScopeRequirement["Required"] = 0] = "Required"; - ScopeRequirement[ScopeRequirement["Optional"] = 1] = "Optional"; - ScopeRequirement[ScopeRequirement["Forbidden"] = 2] = "Forbidden"; -})(ScopeRequirement || (ScopeRequirement = {})); -var ReleaseNotesLevel; -(function (ReleaseNotesLevel) { - ReleaseNotesLevel[ReleaseNotesLevel["Hidden"] = 0] = "Hidden"; - ReleaseNotesLevel[ReleaseNotesLevel["Visible"] = 1] = "Visible"; -})(ReleaseNotesLevel || (ReleaseNotesLevel = {})); -/** The valid commit types for Angular commit messages. */ -const COMMIT_TYPES = { - 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, - }, -}; - -/** - * @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 - */ -/** - * 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 commit fields described as git log format entries for parsing. */ -const commitFieldsAsFormat = (fields) => { - 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. - * */ -const gitLogFormatForParsing = `%B${commitFieldsAsFormat(commitFields)}`; -/** Markers used to denote the start of a note section in a commit. */ -var NoteSections; -(function (NoteSections) { - NoteSections["BREAKING_CHANGE"] = "BREAKING CHANGE"; - NoteSections["DEPRECATED"] = "DEPRECATED"; -})(NoteSections || (NoteSections = {})); -/** 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 = { - commentChar: '#', - headerPattern, - headerCorrespondence, - noteKeywords: [NoteSections.BREAKING_CHANGE, NoteSections.DEPRECATED], - notesPattern: (keywords) => new RegExp(`^\s*(${keywords}): ?(.*)`), -}; -/** Parse a commit message into its composite parts. */ -const parseCommitMessage = parseInternal; -/** Parse a commit message from a git log entry into its composite parts. */ -const parseCommitFromGitLog = parseInternal; -function parseInternal(fullText) { - // 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 = conventionalCommitsParser.sync(strippedCommitMsg, parseOptions); - /** A list of breaking change notes from the commit. */ - const breakingChanges = []; - /** A list of deprecation notes from the commit. */ - const deprecations = []; - // Extract the commit message notes by marked types into their respective lists. - commit.notes.forEach((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, - }; -} - -/** - * @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 - */ -/** 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 ` | Here we assume the colon is missing by accident. - * - `BREAKING-CHANGE: ` | The wrong keyword is used here. - * - `BREAKING CHANGES: ` | The wrong keyword is used here. - * - `BREAKING-CHANGES: ` | 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 ` | Here we assume the colon is missing by accident. - * - `DEPRECATIONS: ` | The wrong keyword is used here. - * - `DEPRECATE: ` | The wrong keyword is used here. - * - `DEPRECATES: ` | 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. */ -function validateCommitMessage(commitMsg, options = {}) { - const config = getCommitMessageConfig().commitMessage; - const commit = typeof commitMsg === 'string' ? parseCommitMessage(commitMsg) : commitMsg; - const errors = []; - /** Perform the validation checks against the parsed commit. */ - function validateCommitAndCollectErrors() { - //////////////////////////////////// - // Checking revert, squash, fixup // - //////////////////////////////////// - var _a; - // 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 (!((_a = config.minBodyLengthTypeExcludes) === null || _a === void 0 ? void 0 : _a.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) => { - // 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. */ -function printValidationErrors(errors, 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('(): '); - print(); - print(''); - print(); - print(`BREAKING CHANGE: `); - print(); - print(``); - print(); - print(); -} - -/** - * @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 - */ -/** Validate commit message at the provided file path. */ -function validateFile(filePath, isErrorMode) { - const git = GitClient.get(); - const commitMessage = fs.readFileSync(path.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; -} - -/** - * @license - * Copyright Google LLC All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ -/** Builds the command. */ -function builder$3(yargs) { - var _a; - return yargs - .option('file', { - type: 'string', - conflicts: ['file-env-variable'], - description: 'The path of the commit message file.', - }) - .option('file-env-variable', { - type: 'string', - conflicts: ['file'], - description: 'The key of the environment variable for the path of the commit message file.', - coerce: (arg) => { - 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: !!((_a = getUserConfig().commitMessage) === null || _a === void 0 ? void 0 : _a.errorOnInvalidMessage) || !!process.env['CI'] - }); -} -/** Handles the command. */ -function handler$3({ error, file, fileEnvVariable }) { - return tslib.__awaiter(this, void 0, void 0, function* () { - const filePath = file || fileEnvVariable || '.git/COMMIT_EDITMSG'; - validateFile(filePath, error); - }); -} -/** yargs command module describing the command. */ -const ValidateFileModule = { - handler: handler$3, - builder: builder$3, - command: 'pre-commit-validate', - describe: 'Validate the most recent commit message', -}; - -/** - * @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 - */ -// 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. - */ -function getCommitsInRange(from, to = 'HEAD') { - return new Promise((resolve, reject) => { - /** List of parsed commit objects. */ - const commits = []; - /** 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) => commits.push(parseCommitFromGitLog(commit))); - commitStream.on('error', (err) => reject(err)); - commitStream.on('end', () => resolve(commits)); - }); -} - -// Whether the provided commit is a fixup commit. -const isNonFixup = (commit) => !commit.isFixup; -// Extracts commit header (first line of commit message). -const extractCommitHeader = (commit) => commit.header; -/** Validate all commits in a provided git commit range. */ -function validateCommitRange(from, to) { - return tslib.__awaiter(this, void 0, void 0, function* () { - /** A list of tuples of the commit header string and a list of error messages for the commit. */ - const errors = []; - /** A list of parsed commit messages from the range. */ - const commits = yield 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 = { - 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); - } - }); -} - -/** - * @license - * Copyright Google LLC All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ -/** Builds the command. */ -function builder$4(yargs) { - 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. */ -function handler$4({ startingRef, endingRef }) { - return tslib.__awaiter(this, void 0, void 0, function* () { - // 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; - } - yield validateCommitRange(startingRef, endingRef); - }); -} -/** yargs command module describing the command. */ -const ValidateRangeModule = { - handler: handler$4, - builder: builder$4, - command: 'validate-range [ending-ref]', - describe: 'Validate a range of commit messages', -}; - -/** Build the parser for the commit-message commands. */ -function buildCommitMessageParser(localYargs) { - return localYargs.help() - .strict() - .command(RestoreCommitMessageModule) - .command(ValidateFileModule) - .command(ValidateRangeModule); -} - -/** - * @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 - */ -/** - * Spawns a given command with the specified arguments inside an interactive shell. All process - * stdin, stdout and stderr output is printed to the current console. - * - * @returns a Promise resolving on success, and rejecting on command failure with the status code. - */ -function spawnInteractive(command, args, options) { - if (options === void 0) { options = {}; } - return new Promise(function (resolve, reject) { - var commandText = command + " " + args.join(' '); - debug("Executing command: " + commandText); - var childProcess = child_process.spawn(command, args, tslib.__assign(tslib.__assign({}, options), { shell: true, stdio: 'inherit' })); - childProcess.on('exit', function (status) { return status === 0 ? resolve() : reject(status); }); - }); -} -/** - * Spawns a given command with the specified arguments inside a shell. All process stdout - * output is captured and returned as resolution on completion. Depending on the chosen - * output mode, stdout/stderr output is also printed to the console, or only on error. - * - * @returns a Promise resolving with captured stdout and stderr on success. The promise - * rejects on command failure. - */ -function spawn(command, args, options) { - if (options === void 0) { options = {}; } - return new Promise(function (resolve, reject) { - var commandText = command + " " + args.join(' '); - var outputMode = options.mode; - debug("Executing command: " + commandText); - var childProcess = child_process.spawn(command, args, tslib.__assign(tslib.__assign({}, options), { shell: true, stdio: 'pipe' })); - var logOutput = ''; - var stdout = ''; - var stderr = ''; - // Capture the stdout separately so that it can be passed as resolve value. - // This is useful if commands return parsable stdout. - childProcess.stderr.on('data', function (message) { - stderr += message; - logOutput += message; - // If console output is enabled, print the message directly to the stderr. Note that - // we intentionally print all output to stderr as stdout should not be polluted. - if (outputMode === undefined || outputMode === 'enabled') { - process.stderr.write(message); - } - }); - childProcess.stdout.on('data', function (message) { - stdout += message; - logOutput += message; - // If console output is enabled, print the message directly to the stderr. Note that - // we intentionally print all output to stderr as stdout should not be polluted. - if (outputMode === undefined || outputMode === 'enabled') { - process.stderr.write(message); - } - }); - childProcess.on('exit', function (exitCode, signal) { - var exitDescription = exitCode !== null ? "exit code \"" + exitCode + "\"" : "signal \"" + signal + "\""; - var printFn = outputMode === 'on-error' ? error : debug; - var status = statusFromExitCodeAndSignal(exitCode, signal); - printFn("Command \"" + commandText + "\" completed with " + exitDescription + "."); - printFn("Process output: \n" + logOutput); - // On success, resolve the promise. Otherwise reject with the captured stderr - // and stdout log output if the output mode was set to `silent`. - if (status === 0 || options.suppressErrorOnFailingExitCode) { - resolve({ stdout: stdout, stderr: stderr, status: status }); - } - else { - reject(outputMode === 'silent' ? logOutput : undefined); - } - }); - }); -} -/** - * Spawns a given command with the specified arguments inside a shell synchronously. - * - * @returns The command's stdout and stderr. - */ -function spawnSync(command, args, options) { - if (options === void 0) { options = {}; } - var commandText = command + " " + args.join(' '); - debug("Executing command: " + commandText); - var _a = child_process.spawnSync(command, args, tslib.__assign(tslib.__assign({}, options), { encoding: 'utf8', shell: true, stdio: 'pipe' })), exitCode = _a.status, signal = _a.signal, stdout = _a.stdout, stderr = _a.stderr; - /** The status of the spawn result. */ - var status = statusFromExitCodeAndSignal(exitCode, signal); - if (status === 0 || options.suppressErrorOnFailingExitCode) { - return { status: status, stdout: stdout, stderr: stderr }; - } - throw new Error(stderr); -} -/** - * Convert the provided exitCode and signal to a single status code. - * - * During `exit` node provides either a `code` or `signal`, one of which is guaranteed to be - * non-null. - * - * For more details see: https://nodejs.org/api/child_process.html#child_process_event_exit - */ -function statusFromExitCodeAndSignal(exitCode, signal) { - var _a; - return (_a = exitCode !== null && exitCode !== void 0 ? exitCode : signal) !== null && _a !== void 0 ? _a : -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 `FormatConfig`. */ -function getFormatConfig() { - // List of errors encountered validating the config. - const errors = []; - // The unvalidated config object. - const config = 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; -} -/** Validate an individual Formatter config. */ -function checkFormatterConfig(key, config, errors) { - if (config.matchers === undefined) { - errors.push(`Missing "format.${key}.matchers" value`); - } -} - -/** - * @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 base class for formatters to run against provided files. - */ -class Formatter { - constructor(config) { - this.config = config; - this.git = GitClient.get(); - } - /** - * Retrieve the command to execute the provided action, including both the binary - * and command line flags. - */ - commandFor(action) { - 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) { - 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. - */ - getFileMatcherFromConfig() { - const formatterConfig = this.config[this.name]; - if (typeof formatterConfig === 'boolean') { - return undefined; - } - return formatterConfig.matchers; - } -} - -/** - * @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 - */ -/** - * Formatter for running buildifier against bazel related files. - */ -class Buildifier extends Formatter { - constructor() { - super(...arguments); - this.name = 'buildifier'; - this.binaryFilePath = path.join(this.git.baseDir, 'node_modules/.bin/buildifier'); - this.defaultFileMatcher = ['**/*.bzl', '**/BUILD.bazel', '**/WORKSPACE', '**/BUILD']; - this.actions = { - check: { - commandFlags: `${BAZEL_WARNING_FLAG} --lint=warn --mode=check --format=json`, - callback: (_, code, stdout) => { - return code !== 0 || !JSON.parse(stdout).success; - }, - }, - format: { - commandFlags: `${BAZEL_WARNING_FLAG} --lint=fix --mode=fix`, - callback: (file, code, _, stderr) => { - 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`; - -/** - * @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 - */ -/** - * Formatter for running clang-format against Typescript and Javascript files - */ -class ClangFormat extends Formatter { - constructor() { - super(...arguments); - this.name = 'clang-format'; - this.binaryFilePath = path.join(this.git.baseDir, 'node_modules/.bin/clang-format'); - this.defaultFileMatcher = ['**/*.{t,j}s']; - this.actions = { - check: { - commandFlags: `--Werror -n -style=file`, - callback: (_, code) => { - return code !== 0; - }, - }, - format: { - commandFlags: `-i -style=file`, - callback: (file, code, _, stderr) => { - if (code !== 0) { - error(`Error running clang-format on: ${file}`); - error(stderr); - error(); - return true; - } - 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 - */ -/** - * Formatter for running prettier against Typescript and Javascript files. - */ -class Prettier extends Formatter { - constructor() { - super(...arguments); - this.name = 'prettier'; - this.binaryFilePath = path.join(this.git.baseDir, 'node_modules/.bin/prettier'); - this.defaultFileMatcher = ['**/*.{t,j}s']; - /** - * The configuration path of the prettier config, obtained during construction to prevent needing - * to discover it repeatedly for each execution. - */ - this.configPath = this.config['prettier'] ? - spawnSync(this.binaryFilePath, ['--find-config-path', '.']).stdout.trim() : - ''; - this.actions = { - check: { - commandFlags: `--config ${this.configPath} --check`, - callback: (_, code, stdout) => { - return code !== 0; - }, - }, - format: { - commandFlags: `--config ${this.configPath} --write`, - callback: (file, code, _, stderr) => { - if (code !== 0) { - error(`Error running prettier on: ${file}`); - error(stderr); - error(); - return true; - } - 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 - */ -/** - * Get all defined formatters which are active based on the current loaded config. - */ -function getActiveFormatters() { - const config = getFormatConfig().format; - return [ - new Prettier(config), - new Buildifier(config), - new ClangFormat(config), - ].filter((formatter) => formatter.isEnabled()); -} - -/** - * @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 AVAILABLE_THREADS = Math.max(os.cpus().length - 1, 1); -/** - * Run the provided commands in parallel for each provided file. - * - * Running the formatter is split across (number of available cpu threads - 1) processess. - * The task is done in multiple processess to speed up the overall time of the task, as running - * across entire repositories takes a large amount of time. - * As a data point for illustration, using 8 process rather than 1 cut the execution - * time from 276 seconds to 39 seconds for the same 2700 files. - * - * A promise is returned, completed when the command has completed running for each file. - * The promise resolves with a list of failures, or `false` if no formatters have matched. - */ -function runFormatterInParallel(allFiles, action) { - return new Promise((resolve) => { - const formatters = getActiveFormatters(); - const failures = []; - const pendingCommands = []; - for (const formatter of formatters) { - pendingCommands.push(...multimatch.call(undefined, allFiles, formatter.getFileMatcher(), { dot: true }) - .map(file => ({ formatter, file }))); - } - // If no commands are generated, resolve the promise as `false` as no files - // were run against the any formatters. - if (pendingCommands.length === 0) { - return resolve(false); - } - switch (action) { - case 'format': - info(`Formatting ${pendingCommands.length} file(s)`); - break; - case 'check': - info(`Checking format of ${pendingCommands.length} file(s)`); - break; - default: - throw Error(`Invalid format action "${action}": allowed actions are "format" and "check"`); - } - // The progress bar instance to use for progress tracking. - const progressBar = new cliProgress.Bar({ format: `[{bar}] ETA: {eta}s | {value}/{total} files`, clearOnComplete: true }); - // A local copy of the files to run the command on. - // An array to represent the current usage state of each of the threads for parallelization. - const threads = new Array(AVAILABLE_THREADS).fill(false); - // Recursively run the command on the next available file from the list using the provided - // thread. - function runCommandInThread(thread) { - const nextCommand = pendingCommands.pop(); - // If no file was pulled from the array, return as there are no more files to run against. - if (nextCommand === undefined) { - threads[thread] = false; - return; - } - // Get the file and formatter for the next command. - const { file, formatter } = nextCommand; - const [spawnCmd, ...spawnArgs] = [...formatter.commandFor(action).split(' '), file]; - spawn(spawnCmd, spawnArgs, { suppressErrorOnFailingExitCode: true, mode: 'silent' }) - .then(({ stdout, stderr, status }) => { - // Run the provided callback function. - const failed = formatter.callbackFor(action)(file, status, stdout, stderr); - if (failed) { - failures.push({ filePath: file, message: stderr }); - } - // Note in the progress bar another file being completed. - progressBar.increment(1); - // If more files exist in the list, run again to work on the next file, - // using the same slot. - if (pendingCommands.length) { - return runCommandInThread(thread); - } - // If not more files are available, mark the thread as unused. - threads[thread] = false; - // If all of the threads are false, as they are unused, mark the progress bar - // completed and resolve the promise. - if (threads.every(active => !active)) { - progressBar.stop(); - resolve(failures); - } - }); - // Mark the thread as in use as the command execution has been started. - threads[thread] = true; - } - // Start the progress bar - progressBar.start(pendingCommands.length, 0); - // Start running the command on files from the least in each available thread. - threads.forEach((_, idx) => runCommandInThread(idx)); - }); -} - -/** - * @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 - */ -/** - * Format provided files in place. - */ -function formatFiles(files) { - return tslib.__awaiter(this, void 0, void 0, function* () { - // Whether any files failed to format. - let failures = yield 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. - */ -function checkFiles(files) { - return tslib.__awaiter(this, void 0, void 0, function* () { - // Files which are currently not formatted correctly. - const failures = yield 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 = yield promptConfirm('Format the files now?', true); - } - if (runFormatter) { - // Format the failing files as requested. - yield 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); - } - }); -} - -/** Build the parser for the format commands. */ -function buildFormatParser(localYargs) { - 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 ', 'Run the formatter on provided files', args => args.positional('files', { array: true, type: 'string' }), ({ check, files }) => { - const executionCmd = check ? checkFiles : formatFiles; - executionCmd(files); - }); -} - -/** - * @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 - */ -function verify() { - const git = GitClient.get(); - /** Full path to NgBot config file */ - const NGBOT_CONFIG_YAML_PATH = path.resolve(git.baseDir, '.github/angular-robot.yml'); - /** The NgBot config file */ - const ngBotYaml = fs.readFileSync(NGBOT_CONFIG_YAML_PATH, 'utf8'); - try { - // Try parsing the config file to verify that the syntax is correct. - yaml.parse(ngBotYaml); - info(`${green('√')} Valid NgBot YAML config`); - } - catch (e) { - error(`${red('!')} Invalid NgBot YAML config`); - error(e); - process.exitCode = 1; - } -} - -/** Build the parser for the NgBot commands. */ -function buildNgbotParser(localYargs) { - return localYargs.help().strict().demandCommand().command('verify', 'Verify the NgBot config', {}, () => verify()); -} - -/** - * @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 - */ -/** Loads and validates the merge configuration. */ -function loadAndValidateConfig(config, api) { - return tslib.__awaiter(this, void 0, void 0, function () { - var mergeConfig, errors; - return tslib.__generator(this, function (_a) { - switch (_a.label) { - case 0: - if (config.merge === undefined) { - return [2 /*return*/, { errors: ['No merge configuration found. Set the `merge` configuration.'] }]; - } - if (typeof config.merge !== 'function') { - return [2 /*return*/, { errors: ['Expected merge configuration to be defined lazily through a function.'] }]; - } - return [4 /*yield*/, config.merge(api)]; - case 1: - mergeConfig = _a.sent(); - errors = validateMergeConfig(mergeConfig); - if (errors.length) { - return [2 /*return*/, { errors: errors }]; - } - return [2 /*return*/, { config: mergeConfig }]; - } - }); - }); -} -/** Validates the specified configuration. Returns a list of failure messages. */ -function validateMergeConfig(config) { - var errors = []; - if (!config.labels) { - errors.push('No label configuration.'); - } - else if (!Array.isArray(config.labels)) { - errors.push('Label configuration needs to be an array.'); - } - if (!config.claSignedLabel) { - errors.push('No CLA signed label configured.'); - } - if (!config.mergeReadyLabel) { - errors.push('No merge ready label configured.'); - } - if (config.githubApiMerge === undefined) { - errors.push('No explicit choice of merge strategy. Please set `githubApiMerge`.'); - } - return errors; -} - -/** - * @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 - */ -/** Checks whether the specified value matches the given pattern. */ -function matchesPattern(value, pattern) { - return typeof pattern === 'string' ? value === pattern : pattern.test(value); -} - -/** - * @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 - */ -/** - * Unique error that can be thrown in the merge configuration if an - * invalid branch is targeted. - */ -var InvalidTargetBranchError = /** @class */ (function () { - function InvalidTargetBranchError(failureMessage) { - this.failureMessage = failureMessage; - } - return InvalidTargetBranchError; -}()); -/** - * Unique error that can be thrown in the merge configuration if an - * invalid label has been applied to a pull request. - */ -var InvalidTargetLabelError = /** @class */ (function () { - function InvalidTargetLabelError(failureMessage) { - this.failureMessage = failureMessage; - } - return InvalidTargetLabelError; -}()); -/** Gets the target label from the specified pull request labels. */ -function getTargetLabelFromPullRequest(config, labels) { - var e_1, _a; - /** List of discovered target labels for the PR. */ - var matches = []; - var _loop_1 = function (label) { - var match = config.labels.find(function (_a) { - var pattern = _a.pattern; - return matchesPattern(label, pattern); - }); - if (match !== undefined) { - matches.push(match); - } - }; - try { - for (var labels_1 = tslib.__values(labels), labels_1_1 = labels_1.next(); !labels_1_1.done; labels_1_1 = labels_1.next()) { - var label = labels_1_1.value; - _loop_1(label); - } - } - catch (e_1_1) { e_1 = { error: e_1_1 }; } - finally { - try { - if (labels_1_1 && !labels_1_1.done && (_a = labels_1.return)) _a.call(labels_1); - } - finally { if (e_1) throw e_1.error; } - } - if (matches.length === 1) { - return matches[0]; - } - if (matches.length === 0) { - throw new InvalidTargetLabelError('Unable to determine target for the PR as it has no target label.'); - } - throw new InvalidTargetLabelError('Unable to determine target for the PR as it has multiple target labels.'); -} -/** - * Gets the branches from the specified target label. - * - * @throws {InvalidTargetLabelError} Invalid label has been applied to pull request. - * @throws {InvalidTargetBranchError} Invalid Github target branch has been selected. - */ -function getBranchesFromTargetLabel(label, githubTargetBranch) { - return tslib.__awaiter(this, void 0, void 0, function () { - var _a; - return tslib.__generator(this, function (_b) { - switch (_b.label) { - case 0: - if (!(typeof label.branches === 'function')) return [3 /*break*/, 2]; - return [4 /*yield*/, label.branches(githubTargetBranch)]; - case 1: - _a = _b.sent(); - return [3 /*break*/, 4]; - case 2: return [4 /*yield*/, label.branches]; - case 3: - _a = _b.sent(); - _b.label = 4; - case 4: return [2 /*return*/, _a]; - } - }); - }); -} - -/** - * @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 - */ -function getTargetBranchesForPr(prNumber) { - return tslib.__awaiter(this, void 0, void 0, function* () { - /** The ng-dev configuration. */ - const config = getConfig(); - /** Repo owner and name for the github repository. */ - const { owner, name: repo } = config.github; - /** The singleton instance of the GitClient. */ - const git = GitClient.get(); - /** The validated merge config. */ - const { config: mergeConfig, errors } = yield loadAndValidateConfig(config, git.github); - if (errors !== undefined) { - throw Error(`Invalid configuration found: ${errors}`); - } - /** The current state of the pull request from Github. */ - const prData = (yield git.github.pulls.get({ owner, repo, pull_number: prNumber })).data; - /** The list of labels on the PR as strings. */ - // Note: The `name` property of labels is always set but the Github OpenAPI spec is incorrect - // here. - // TODO(devversion): Remove the non-null cast once - // https://github.com/github/rest-api-description/issues/169 is fixed. - const labels = prData.labels.map(l => l.name); - /** The branch targetted via the Github UI. */ - const githubTargetBranch = prData.base.ref; - /** The active label which is being used for targetting the PR. */ - let targetLabel; - try { - targetLabel = getTargetLabelFromPullRequest(mergeConfig, labels); - } - catch (e) { - if (e instanceof InvalidTargetLabelError) { - error(red(e.failureMessage)); - process.exitCode = 1; - return; - } - throw e; - } - /** The target branches based on the target label and branch targetted in the Github UI. */ - return yield getBranchesFromTargetLabel(targetLabel, githubTargetBranch); - }); -} -function printTargetBranchesForPr(prNumber) { - return tslib.__awaiter(this, void 0, void 0, function* () { - const targets = yield getTargetBranchesForPr(prNumber); - if (targets === undefined) { - return; - } - info.group(`PR #${prNumber} will merge into:`); - targets.forEach(target => info(`- ${target}`)); - info.groupEnd(); - }); -} - -/** - * @license - * Copyright Google LLC All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ -/** Builds the command. */ -function builder$5(yargs) { - return yargs.positional('pr', { - description: 'The pull request number', - type: 'number', - demandOption: true, - }); -} -/** Handles the command. */ -function handler$5({ pr }) { - return tslib.__awaiter(this, void 0, void 0, function* () { - yield printTargetBranchesForPr(pr); - }); -} -/** yargs command module describing the command. */ -const CheckTargetBranchesModule = { - handler: handler$5, - builder: builder$5, - command: 'check-target-branches ', - describe: 'Check a PR to determine what branches it is currently targeting', -}; - -/** - * @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 - */ -/** Get a PR from github */ -function getPr(prSchema, prNumber, git) { - return tslib.__awaiter(this, void 0, void 0, function () { - var _a, owner, name, PR_QUERY, result; - return tslib.__generator(this, function (_b) { - switch (_b.label) { - case 0: - _a = git.remoteConfig, owner = _a.owner, name = _a.name; - PR_QUERY = typedGraphqlify.params({ - $number: 'Int!', - $owner: 'String!', - $name: 'String!', // The organization to query for - }, { - repository: typedGraphqlify.params({ owner: '$owner', name: '$name' }, { - pullRequest: typedGraphqlify.params({ number: '$number' }, prSchema), - }) - }); - return [4 /*yield*/, git.github.graphql(PR_QUERY, { number: prNumber, owner: owner, name: name })]; - case 1: - result = (_b.sent()); - return [2 /*return*/, result.repository.pullRequest]; - } - }); - }); -} -/** Get all pending PRs from github */ -function getPendingPrs(prSchema, git) { - return tslib.__awaiter(this, void 0, void 0, function () { - var _a, owner, name, PRS_QUERY, cursor, hasNextPage, prs, params_1, results; - return tslib.__generator(this, function (_b) { - switch (_b.label) { - case 0: - _a = git.remoteConfig, owner = _a.owner, name = _a.name; - PRS_QUERY = typedGraphqlify.params({ - $first: 'Int', - $after: 'String', - $owner: 'String!', - $name: 'String!', // The repository to query for - }, { - repository: typedGraphqlify.params({ owner: '$owner', name: '$name' }, { - pullRequests: typedGraphqlify.params({ - first: '$first', - after: '$after', - states: "OPEN", - }, { - nodes: [prSchema], - pageInfo: { - hasNextPage: typedGraphqlify.types.boolean, - endCursor: typedGraphqlify.types.string, - }, - }), - }) - }); - hasNextPage = true; - prs = []; - _b.label = 1; - case 1: - if (!hasNextPage) return [3 /*break*/, 3]; - params_1 = { - after: cursor || null, - first: 100, - owner: owner, - name: name, - }; - return [4 /*yield*/, git.github.graphql(PRS_QUERY, params_1)]; - case 2: - results = _b.sent(); - prs.push.apply(prs, tslib.__spreadArray([], tslib.__read(results.repository.pullRequests.nodes))); - hasNextPage = results.repository.pullRequests.pageInfo.hasNextPage; - cursor = results.repository.pullRequests.pageInfo.endCursor; - return [3 /*break*/, 1]; - case 3: return [2 /*return*/, prs]; - } - }); - }); -} - -/** - * @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 - */ -/* Graphql schema for the response body for a pending PR. */ -const PR_SCHEMA = { - state: typedGraphqlify.types.string, - maintainerCanModify: typedGraphqlify.types.boolean, - viewerDidAuthor: typedGraphqlify.types.boolean, - headRefOid: typedGraphqlify.types.string, - headRef: { - name: typedGraphqlify.types.string, - repository: { - url: typedGraphqlify.types.string, - nameWithOwner: typedGraphqlify.types.string, - }, - }, - baseRef: { - name: typedGraphqlify.types.string, - repository: { - url: typedGraphqlify.types.string, - nameWithOwner: typedGraphqlify.types.string, - }, - }, -}; -class UnexpectedLocalChangesError extends Error { - constructor(m) { - super(m); - Object.setPrototypeOf(this, UnexpectedLocalChangesError.prototype); - } -} -class MaintainerModifyAccessError extends Error { - constructor(m) { - super(m); - Object.setPrototypeOf(this, MaintainerModifyAccessError.prototype); - } -} -/** - * Rebase the provided PR onto its merge target branch, and push up the resulting - * commit to the PRs repository. - */ -function checkOutPullRequestLocally(prNumber, githubToken, opts = {}) { - return tslib.__awaiter(this, void 0, void 0, function* () { - /** The singleton instance of the authenticated git client. */ - const git = AuthenticatedGitClient.get(); - // In order to preserve local changes, checkouts cannot occur if local changes are present in the - // git environment. Checked before retrieving the PR to fail fast. - if (git.hasUncommittedChanges()) { - throw new UnexpectedLocalChangesError('Unable to checkout PR due to uncommitted changes.'); - } - /** - * The branch or revision originally checked out before this method performed - * any Git operations that may change the working branch. - */ - const previousBranchOrRevision = git.getCurrentBranchOrRevision(); - /* The PR information from Github. */ - const pr = yield getPr(PR_SCHEMA, prNumber, git); - /** The branch name of the PR from the repository the PR came from. */ - const headRefName = pr.headRef.name; - /** The full ref for the repository and branch the PR came from. */ - const fullHeadRef = `${pr.headRef.repository.nameWithOwner}:${headRefName}`; - /** The full URL path of the repository the PR came from with github token as authentication. */ - const headRefUrl = addTokenToGitHttpsUrl(pr.headRef.repository.url, githubToken); - // Note: Since we use a detached head for rebasing the PR and therefore do not have - // remote-tracking branches configured, we need to set our expected ref and SHA. This - // allows us to use `--force-with-lease` for the detached head while ensuring that we - // never accidentally override upstream changes that have been pushed in the meanwhile. - // See: - // https://git-scm.com/docs/git-push#Documentation/git-push.txt---force-with-leaseltrefnamegtltexpectgt - /** Flag for a force push with lease back to upstream. */ - const forceWithLeaseFlag = `--force-with-lease=${headRefName}:${pr.headRefOid}`; - // If the PR does not allow maintainers to modify it, exit as the rebased PR cannot - // be pushed up. - if (!pr.maintainerCanModify && !pr.viewerDidAuthor && !opts.allowIfMaintainerCannotModify) { - throw new MaintainerModifyAccessError('PR is not set to allow maintainers to modify the PR'); - } - try { - // Fetch the branch at the commit of the PR, and check it out in a detached state. - info(`Checking out PR #${prNumber} from ${fullHeadRef}`); - git.run(['fetch', '-q', headRefUrl, headRefName]); - git.run(['checkout', '--detach', 'FETCH_HEAD']); - } - catch (e) { - git.checkout(previousBranchOrRevision, true); - throw e; - } - return { - /** - * Pushes the current local branch to the PR on the upstream repository. - * - * @returns true If the command did not fail causing a GitCommandError to be thrown. - * @throws GitCommandError Thrown when the push back to upstream fails. - */ - pushToUpstream: () => { - git.run(['push', headRefUrl, `HEAD:${headRefName}`, forceWithLeaseFlag]); - return true; - }, - /** Restores the state of the local repository to before the PR checkout occured. */ - resetGitState: () => { - return git.checkout(previousBranchOrRevision, true); - } - }; - }); -} - -/** - * @license - * Copyright Google LLC All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ -/** Builds the checkout pull request command. */ -function builder$6(yargs) { - return addGithubTokenOption(yargs).positional('prNumber', { type: 'number', demandOption: true }); -} -/** Handles the checkout pull request command. */ -function handler$6({ prNumber, githubToken }) { - return tslib.__awaiter(this, void 0, void 0, function* () { - const prCheckoutOptions = { allowIfMaintainerCannotModify: true, branchName: `pr-${prNumber}` }; - yield checkOutPullRequestLocally(prNumber, githubToken, prCheckoutOptions); - }); -} -/** yargs command module for checking out a PR */ -const CheckoutCommandModule = { - handler: handler$6, - builder: builder$6, - command: 'checkout ', - describe: 'Checkout a PR from the upstream repo', -}; - -/** - * @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 - */ -/* Graphql schema for the response body for each pending PR. */ -const PR_SCHEMA$1 = { - headRef: { - name: typedGraphqlify.types.string, - repository: { - url: typedGraphqlify.types.string, - nameWithOwner: typedGraphqlify.types.string, - }, - }, - baseRef: { - name: typedGraphqlify.types.string, - repository: { - url: typedGraphqlify.types.string, - nameWithOwner: typedGraphqlify.types.string, - }, - }, - updatedAt: typedGraphqlify.types.string, - number: typedGraphqlify.types.number, - mergeable: typedGraphqlify.types.string, - title: typedGraphqlify.types.string, -}; -/** Convert raw Pull Request response from Github to usable Pull Request object. */ -function processPr(pr) { - return Object.assign(Object.assign({}, pr), { updatedAt: (new Date(pr.updatedAt)).getTime() }); -} -/** Name of a temporary local branch that is used for checking conflicts. **/ -const tempWorkingBranch = '__NgDevRepoBaseAfterChange__'; -/** Checks if the provided PR will cause new conflicts in other pending PRs. */ -function discoverNewConflictsForPr(newPrNumber, updatedAfter) { - return tslib.__awaiter(this, void 0, void 0, function* () { - /** The singleton instance of the authenticated git client. */ - const git = AuthenticatedGitClient.get(); - // If there are any local changes in the current repository state, the - // check cannot run as it needs to move between branches. - if (git.hasUncommittedChanges()) { - error('Cannot run with local changes. Please make sure there are no local changes.'); - process.exit(1); - } - /** The active github branch or revision before we performed any Git commands. */ - const previousBranchOrRevision = git.getCurrentBranchOrRevision(); - /* Progress bar to indicate progress. */ - const progressBar = new cliProgress.Bar({ format: `[{bar}] ETA: {eta}s | {value}/{total}` }); - /* PRs which were found to be conflicting. */ - const conflicts = []; - info(`Requesting pending PRs from Github`); - /** List of PRs from github currently known as mergeable. */ - const allPendingPRs = (yield getPendingPrs(PR_SCHEMA$1, git)).map(processPr); - /** The PR which is being checked against. */ - const requestedPr = allPendingPRs.find(pr => pr.number === newPrNumber); - if (requestedPr === undefined) { - error(`The request PR, #${newPrNumber} was not found as a pending PR on github, please confirm`); - error(`the PR number is correct and is an open PR`); - process.exit(1); - } - const pendingPrs = allPendingPRs.filter(pr => { - return ( - // PRs being merged into the same target branch as the requested PR - pr.baseRef.name === requestedPr.baseRef.name && - // PRs which either have not been processed or are determined as mergeable by Github - pr.mergeable !== 'CONFLICTING' && - // PRs updated after the provided date - pr.updatedAt >= updatedAfter); - }); - info(`Retrieved ${allPendingPRs.length} total pending PRs`); - info(`Checking ${pendingPrs.length} PRs for conflicts after a merge of #${newPrNumber}`); - // Fetch and checkout the PR being checked. - git.run(['fetch', '-q', requestedPr.headRef.repository.url, requestedPr.headRef.name]); - git.run(['checkout', '-q', '-B', tempWorkingBranch, 'FETCH_HEAD']); - // Rebase the PR against the PRs target branch. - git.run(['fetch', '-q', requestedPr.baseRef.repository.url, requestedPr.baseRef.name]); - try { - git.run(['rebase', 'FETCH_HEAD'], { stdio: 'ignore' }); - } - catch (err) { - if (err instanceof GitCommandError) { - error('The requested PR currently has conflicts'); - git.checkout(previousBranchOrRevision, true); - process.exit(1); - } - throw err; - } - // Start the progress bar - progressBar.start(pendingPrs.length, 0); - // Check each PR to determine if it can merge cleanly into the repo after the target PR. - for (const pr of pendingPrs) { - // Fetch and checkout the next PR - git.run(['fetch', '-q', pr.headRef.repository.url, pr.headRef.name]); - git.run(['checkout', '-q', '--detach', 'FETCH_HEAD']); - // Check if the PR cleanly rebases into the repo after the target PR. - try { - git.run(['rebase', tempWorkingBranch], { stdio: 'ignore' }); - } - catch (err) { - if (err instanceof GitCommandError) { - conflicts.push(pr); - } - else { - throw err; - } - } - // Abort any outstanding rebase attempt. - git.runGraceful(['rebase', '--abort'], { stdio: 'ignore' }); - progressBar.increment(1); - } - // End the progress bar as all PRs have been processed. - progressBar.stop(); - info(); - info(`Result:`); - git.checkout(previousBranchOrRevision, true); - // If no conflicts are found, exit successfully. - if (conflicts.length === 0) { - info(`No new conflicting PRs found after #${newPrNumber} merging`); - process.exit(0); - } - // Inform about discovered conflicts, exit with failure. - error.group(`${conflicts.length} PR(s) which conflict(s) after #${newPrNumber} merges:`); - for (const pr of conflicts) { - error(` - #${pr.number}: ${pr.title}`); - } - error.groupEnd(); - 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 - */ -/** Builds the discover-new-conflicts pull request command. */ -function buildDiscoverNewConflictsCommand(yargs) { - return addGithubTokenOption(yargs) - .option('date', { - description: 'Only consider PRs updated since provided date', - defaultDescription: '30 days ago', - coerce: (date) => typeof date === 'number' ? date : Date.parse(date), - default: getThirtyDaysAgoDate(), - }) - .positional('pr-number', { demandOption: true, type: 'number' }); -} -/** Handles the discover-new-conflicts pull request command. */ -function handleDiscoverNewConflictsCommand({ 'pr-number': prNumber, date }) { - return tslib.__awaiter(this, void 0, void 0, function* () { - // If a provided date is not able to be parsed, yargs provides it as NaN. - if (isNaN(date)) { - error('Unable to parse the value provided via --date flag'); - process.exit(1); - } - yield discoverNewConflictsForPr(prNumber, date); - }); -} -/** Gets a date object 30 days ago from today. */ -function getThirtyDaysAgoDate() { - const date = new Date(); - // Set the hours, minutes and seconds to 0 to only consider date. - date.setHours(0, 0, 0, 0); - // Set the date to 30 days in the past. - date.setDate(date.getDate() - 30); - return date.getTime(); -} - -/** - * @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 - */ -/** - * Class that can be used to describe pull request failures. A failure - * is described through a human-readable message and a flag indicating - * whether it is non-fatal or not. - */ -var PullRequestFailure = /** @class */ (function () { - function PullRequestFailure( - /** Human-readable message for the failure */ - message, - /** Whether the failure is non-fatal and can be forcibly ignored. */ - nonFatal) { - if (nonFatal === void 0) { nonFatal = false; } - this.message = message; - this.nonFatal = nonFatal; - } - PullRequestFailure.claUnsigned = function () { - return new this("CLA has not been signed. Please make sure the PR author has signed the CLA."); - }; - PullRequestFailure.failingCiJobs = function () { - return new this("Failing CI jobs.", true); - }; - PullRequestFailure.pendingCiJobs = function () { - return new this("Pending CI jobs.", true); - }; - PullRequestFailure.notMergeReady = function () { - return new this("Not marked as merge ready."); - }; - PullRequestFailure.isDraft = function () { - return new this('Pull request is still in draft.'); - }; - PullRequestFailure.isClosed = function () { - return new this('Pull request is already closed.'); - }; - PullRequestFailure.isMerged = function () { - return new this('Pull request is already merged.'); - }; - PullRequestFailure.mismatchingTargetBranch = function (allowedBranches) { - return new this("Pull request is set to wrong base branch. Please update the PR in the Github UI " + - ("to one of the following branches: " + allowedBranches.join(', ') + ".")); - }; - PullRequestFailure.unsatisfiedBaseSha = function () { - return new this("Pull request has not been rebased recently and could be bypassing CI checks. " + - "Please rebase the PR."); - }; - PullRequestFailure.mergeConflicts = function (failedBranches) { - return new this("Could not merge pull request into the following branches due to merge " + - ("conflicts: " + failedBranches.join(', ') + ". Please rebase the PR or update the target label.")); - }; - PullRequestFailure.unknownMergeError = function () { - return new this("Unknown merge error occurred. Please see console output above for debugging."); - }; - PullRequestFailure.unableToFixupCommitMessageSquashOnly = function () { - return new this("Unable to fixup commit message of pull request. Commit message can only be " + - "modified if the PR is merged using squash."); - }; - PullRequestFailure.notFound = function () { - return new this("Pull request could not be found upstream."); - }; - PullRequestFailure.insufficientPermissionsToMerge = function (message) { - if (message === void 0) { message = "Insufficient Github API permissions to merge pull request. Please ensure that " + - "your auth token has write access."; } - return new this(message); - }; - PullRequestFailure.hasBreakingChanges = function (label) { - var message = "Cannot merge into branch for \"" + label.pattern + "\" as the pull request has " + - "breaking changes. Breaking changes can only be merged with the \"target: major\" label."; - return new this(message); - }; - PullRequestFailure.hasDeprecations = function (label) { - var message = "Cannot merge into branch for \"" + label.pattern + "\" as the pull request " + - "contains deprecations. Deprecations can only be merged with the \"target: minor\" or " + - "\"target: major\" label."; - return new this(message); - }; - PullRequestFailure.hasFeatureCommits = function (label) { - var message = "Cannot merge into branch for \"" + label.pattern + "\" as the pull request has " + - 'commits with the "feat" type. New features can only be merged with the "target: minor" ' + - 'or "target: major" label.'; - return new this(message); - }; - PullRequestFailure.missingBreakingChangeLabel = function () { - var message = 'Pull Request has at least one commit containing a breaking change note, but ' + - 'does not have a breaking change label.'; - return new this(message); - }; - PullRequestFailure.missingBreakingChangeCommit = function () { - var message = 'Pull Request has a breaking change label, but does not contain any commits ' + - 'with breaking change notes.'; - return new this(message); - }; - return PullRequestFailure; -}()); - -/** - * @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 - */ -function getCaretakerNotePromptMessage(pullRequest) { - return red('Pull request has a caretaker note applied. Please make sure you read it.') + - ("\nQuick link to PR: " + pullRequest.url + "\nDo you want to proceed merging?"); -} -function getTargettedBranchesConfirmationPromptMessage(pullRequest) { - var targetBranchListAsString = pullRequest.targetBranches.map(function (b) { return " - " + b + "\n"; }).join(''); - return "Pull request #" + pullRequest.prNumber + " will merge into:\n" + targetBranchListAsString + "\nDo you want to proceed merging?"; -} - -/** - * @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 default label for labeling pull requests containing a breaking change. */ -var BreakingChangeLabel = 'breaking changes'; -/** - * Loads and validates the specified pull request against the given configuration. - * If the pull requests fails, a pull request failure is returned. - */ -function loadAndValidatePullRequest(_a, prNumber, ignoreNonFatalFailures) { - var git = _a.git, config = _a.config; - if (ignoreNonFatalFailures === void 0) { ignoreNonFatalFailures = false; } - return tslib.__awaiter(this, void 0, void 0, function () { - var prData, labels, targetLabel, commitsInPr, state, githubTargetBranch, requiredBaseSha, needsCommitMessageFixup, hasCaretakerNote, targetBranches, error_1; - return tslib.__generator(this, function (_b) { - switch (_b.label) { - case 0: return [4 /*yield*/, fetchPullRequestFromGithub(git, prNumber)]; - case 1: - prData = _b.sent(); - if (prData === null) { - return [2 /*return*/, PullRequestFailure.notFound()]; - } - labels = prData.labels.nodes.map(function (l) { return l.name; }); - if (!labels.some(function (name) { return matchesPattern(name, config.mergeReadyLabel); })) { - return [2 /*return*/, PullRequestFailure.notMergeReady()]; - } - if (!labels.some(function (name) { return matchesPattern(name, config.claSignedLabel); })) { - return [2 /*return*/, PullRequestFailure.claUnsigned()]; - } - try { - targetLabel = getTargetLabelFromPullRequest(config, labels); - } - catch (error) { - if (error instanceof InvalidTargetLabelError) { - return [2 /*return*/, new PullRequestFailure(error.failureMessage)]; - } - throw error; - } - commitsInPr = prData.commits.nodes.map(function (n) { return parseCommitMessage(n.commit.message); }); - try { - assertPendingState(prData); - assertChangesAllowForTargetLabel(commitsInPr, targetLabel, config); - assertCorrectBreakingChangeLabeling(commitsInPr, labels, config); - } - catch (error) { - return [2 /*return*/, error]; - } - state = prData.commits.nodes.slice(-1)[0].commit.status.state; - if (state === 'FAILURE' && !ignoreNonFatalFailures) { - return [2 /*return*/, PullRequestFailure.failingCiJobs()]; - } - if (state === 'PENDING' && !ignoreNonFatalFailures) { - return [2 /*return*/, PullRequestFailure.pendingCiJobs()]; - } - githubTargetBranch = prData.baseRefName; - requiredBaseSha = config.requiredBaseCommits && config.requiredBaseCommits[githubTargetBranch]; - needsCommitMessageFixup = !!config.commitMessageFixupLabel && - labels.some(function (name) { return matchesPattern(name, config.commitMessageFixupLabel); }); - hasCaretakerNote = !!config.caretakerNoteLabel && - labels.some(function (name) { return matchesPattern(name, config.caretakerNoteLabel); }); - _b.label = 2; - case 2: - _b.trys.push([2, 4, , 5]); - return [4 /*yield*/, getBranchesFromTargetLabel(targetLabel, githubTargetBranch)]; - case 3: - targetBranches = _b.sent(); - return [3 /*break*/, 5]; - case 4: - error_1 = _b.sent(); - if (error_1 instanceof InvalidTargetBranchError || error_1 instanceof InvalidTargetLabelError) { - return [2 /*return*/, new PullRequestFailure(error_1.failureMessage)]; - } - throw error_1; - case 5: return [2 /*return*/, { - url: prData.url, - prNumber: prNumber, - labels: labels, - requiredBaseSha: requiredBaseSha, - githubTargetBranch: githubTargetBranch, - needsCommitMessageFixup: needsCommitMessageFixup, - hasCaretakerNote: hasCaretakerNote, - targetBranches: targetBranches, - title: prData.title, - commitCount: prData.commits.totalCount, - }]; - } - }); - }); -} -/* Graphql schema for the response body the requested pull request. */ -var PR_SCHEMA$2 = { - url: typedGraphqlify.types.string, - isDraft: typedGraphqlify.types.boolean, - state: typedGraphqlify.types.oneOf(['OPEN', 'MERGED', 'CLOSED']), - number: typedGraphqlify.types.number, - // Only the last 100 commits from a pull request are obtained as we likely will never see a pull - // requests with more than 100 commits. - commits: typedGraphqlify.params({ last: 100 }, { - totalCount: typedGraphqlify.types.number, - nodes: [{ - commit: { - status: { - state: typedGraphqlify.types.oneOf(['FAILURE', 'PENDING', 'SUCCESS']), - }, - message: typedGraphqlify.types.string, - }, - }], - }), - baseRefName: typedGraphqlify.types.string, - title: typedGraphqlify.types.string, - labels: typedGraphqlify.params({ first: 100 }, { - nodes: [{ - name: typedGraphqlify.types.string, - }] - }), -}; -/** Fetches a pull request from Github. Returns null if an error occurred. */ -function fetchPullRequestFromGithub(git, prNumber) { - return tslib.__awaiter(this, void 0, void 0, function () { - var e_1; - return tslib.__generator(this, function (_a) { - switch (_a.label) { - case 0: - _a.trys.push([0, 2, , 3]); - return [4 /*yield*/, getPr(PR_SCHEMA$2, prNumber, git)]; - case 1: return [2 /*return*/, _a.sent()]; - case 2: - e_1 = _a.sent(); - // If the pull request could not be found, we want to return `null` so - // that the error can be handled gracefully. - if (e_1.status === 404) { - return [2 /*return*/, null]; - } - throw e_1; - case 3: return [2 /*return*/]; - } - }); - }); -} -/** Whether the specified value resolves to a pull request. */ -function isPullRequest(v) { - return v.targetBranches !== undefined; -} -/** - * Assert the commits provided are allowed to merge to the provided target label, - * throwing an error otherwise. - * @throws {PullRequestFailure} - */ -function assertChangesAllowForTargetLabel(commits, label, config) { - /** - * List of commit scopes which are exempted from target label content requirements. i.e. no `feat` - * scopes in patch branches, no breaking changes in minor or patch changes. - */ - var exemptedScopes = config.targetLabelExemptScopes || []; - /** List of commits which are subject to content requirements for the target label. */ - commits = commits.filter(function (commit) { return !exemptedScopes.includes(commit.scope); }); - var hasBreakingChanges = commits.some(function (commit) { return commit.breakingChanges.length !== 0; }); - var hasDeprecations = commits.some(function (commit) { return commit.deprecations.length !== 0; }); - var hasFeatureCommits = commits.some(function (commit) { return commit.type === 'feat'; }); - switch (label.pattern) { - case 'target: major': - break; - case 'target: minor': - if (hasBreakingChanges) { - throw PullRequestFailure.hasBreakingChanges(label); - } - break; - case 'target: rc': - case 'target: patch': - case 'target: lts': - if (hasBreakingChanges) { - throw PullRequestFailure.hasBreakingChanges(label); - } - if (hasFeatureCommits) { - throw PullRequestFailure.hasFeatureCommits(label); - } - // Deprecations should not be merged into RC, patch or LTS branches. - // https://semver.org/#spec-item-7. Deprecations should be part of - // minor releases, or major releases according to SemVer. - if (hasDeprecations) { - throw PullRequestFailure.hasDeprecations(label); - } - break; - default: - warn(red('WARNING: Unable to confirm all commits in the pull request are eligible to be')); - warn(red("merged into the target branch: " + label.pattern)); - break; - } -} -/** - * Assert the pull request has the proper label for breaking changes if there are breaking change - * commits, and only has the label if there are breaking change commits. - * @throws {PullRequestFailure} - */ -function assertCorrectBreakingChangeLabeling(commits, labels, config) { - /** Whether the PR has a label noting a breaking change. */ - var hasLabel = labels.includes(config.breakingChangeLabel || BreakingChangeLabel); - //** Whether the PR has at least one commit which notes a breaking change. */ - var hasCommit = commits.some(function (commit) { return commit.breakingChanges.length !== 0; }); - if (!hasLabel && hasCommit) { - throw PullRequestFailure.missingBreakingChangeLabel(); - } - if (hasLabel && !hasCommit) { - throw PullRequestFailure.missingBreakingChangeCommit(); - } -} -/** - * Assert the pull request is pending, not closed, merged or in draft. - * @throws {PullRequestFailure} if the pull request is not pending. - */ -function assertPendingState(pr) { - if (pr.isDraft) { - throw PullRequestFailure.isDraft(); - } - switch (pr.state) { - case 'CLOSED': - throw PullRequestFailure.isClosed(); - case 'MERGED': - throw PullRequestFailure.isMerged(); - } -} - -/** - * @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 - */ -/** - * Name of a temporary branch that contains the head of a currently-processed PR. Note - * that a branch name should be used that most likely does not conflict with other local - * development branches. - */ -var TEMP_PR_HEAD_BRANCH = 'merge_pr_head'; -/** - * Base class for merge strategies. A merge strategy accepts a pull request and - * merges it into the determined target branches. - */ -var MergeStrategy = /** @class */ (function () { - function MergeStrategy(git) { - this.git = git; - } - /** - * Prepares a merge of the given pull request. The strategy by default will - * fetch all target branches and the pull request into local temporary branches. - */ - MergeStrategy.prototype.prepare = function (pullRequest) { - return tslib.__awaiter(this, void 0, void 0, function () { - return tslib.__generator(this, function (_a) { - this.fetchTargetBranches(pullRequest.targetBranches, "pull/" + pullRequest.prNumber + "/head:" + TEMP_PR_HEAD_BRANCH); - return [2 /*return*/]; - }); - }); - }; - /** Cleans up the pull request merge. e.g. deleting temporary local branches. */ - MergeStrategy.prototype.cleanup = function (pullRequest) { - return tslib.__awaiter(this, void 0, void 0, function () { - var _this = this; - return tslib.__generator(this, function (_a) { - // Delete all temporary target branches. - pullRequest.targetBranches.forEach(function (branchName) { return _this.git.run(['branch', '-D', _this.getLocalTargetBranchName(branchName)]); }); - // Delete temporary branch for the pull request head. - this.git.run(['branch', '-D', TEMP_PR_HEAD_BRANCH]); - return [2 /*return*/]; - }); - }); - }; - /** Gets the revision range for all commits in the given pull request. */ - MergeStrategy.prototype.getPullRequestRevisionRange = function (pullRequest) { - return this.getPullRequestBaseRevision(pullRequest) + ".." + TEMP_PR_HEAD_BRANCH; - }; - /** Gets the base revision of a pull request. i.e. the commit the PR is based on. */ - MergeStrategy.prototype.getPullRequestBaseRevision = function (pullRequest) { - return TEMP_PR_HEAD_BRANCH + "~" + pullRequest.commitCount; - }; - /** Gets a deterministic local branch name for a given branch. */ - MergeStrategy.prototype.getLocalTargetBranchName = function (targetBranch) { - return "merge_pr_target_" + targetBranch.replace(/\//g, '_'); - }; - /** - * Cherry-picks the given revision range into the specified target branches. - * @returns A list of branches for which the revisions could not be cherry-picked into. - */ - MergeStrategy.prototype.cherryPickIntoTargetBranches = function (revisionRange, targetBranches, options) { - var e_1, _a; - if (options === void 0) { options = {}; } - var cherryPickArgs = [revisionRange]; - var failedBranches = []; - if (options.dryRun) { - // https://git-scm.com/docs/git-cherry-pick#Documentation/git-cherry-pick.txt---no-commit - // This causes `git cherry-pick` to not generate any commits. Instead, the changes are - // applied directly in the working tree. This allow us to easily discard the changes - // for dry-run purposes. - cherryPickArgs.push('--no-commit'); - } - if (options.linkToOriginalCommits) { - // We add `-x` when cherry-picking as that will allow us to easily jump to original - // commits for cherry-picked commits. With that flag set, Git will automatically append - // the original SHA/revision to the commit message. e.g. `(cherry picked from commit <..>)`. - // https://git-scm.com/docs/git-cherry-pick#Documentation/git-cherry-pick.txt--x. - cherryPickArgs.push('-x'); - } - try { - // Cherry-pick the refspec into all determined target branches. - for (var targetBranches_1 = tslib.__values(targetBranches), targetBranches_1_1 = targetBranches_1.next(); !targetBranches_1_1.done; targetBranches_1_1 = targetBranches_1.next()) { - var branchName = targetBranches_1_1.value; - var localTargetBranch = this.getLocalTargetBranchName(branchName); - // Checkout the local target branch. - this.git.run(['checkout', localTargetBranch]); - // Cherry-pick the refspec into the target branch. - if (this.git.runGraceful(tslib.__spreadArray(['cherry-pick'], tslib.__read(cherryPickArgs))).status !== 0) { - // Abort the failed cherry-pick. We do this because Git persists the failed - // cherry-pick state globally in the repository. This could prevent future - // pull request merges as a Git thinks a cherry-pick is still in progress. - this.git.runGraceful(['cherry-pick', '--abort']); - failedBranches.push(branchName); - } - // If we run with dry run mode, we reset the local target branch so that all dry-run - // cherry-pick changes are discard. Changes are applied to the working tree and index. - if (options.dryRun) { - this.git.run(['reset', '--hard', 'HEAD']); - } - } - } - catch (e_1_1) { e_1 = { error: e_1_1 }; } - finally { - try { - if (targetBranches_1_1 && !targetBranches_1_1.done && (_a = targetBranches_1.return)) _a.call(targetBranches_1); - } - finally { if (e_1) throw e_1.error; } - } - return failedBranches; - }; - /** - * Fetches the given target branches. Also accepts a list of additional refspecs that - * should be fetched. This is helpful as multiple slow fetches could be avoided. - */ - MergeStrategy.prototype.fetchTargetBranches = function (names) { - var _this = this; - var extraRefspecs = []; - for (var _i = 1; _i < arguments.length; _i++) { - extraRefspecs[_i - 1] = arguments[_i]; - } - var fetchRefspecs = names.map(function (targetBranch) { - var localTargetBranch = _this.getLocalTargetBranchName(targetBranch); - return "refs/heads/" + targetBranch + ":" + localTargetBranch; - }); - // Fetch all target branches with a single command. We don't want to fetch them - // individually as that could cause an unnecessary slow-down. - this.git.run(tslib.__spreadArray(tslib.__spreadArray(['fetch', '-q', '-f', this.git.getRepoGitUrl()], tslib.__read(fetchRefspecs)), tslib.__read(extraRefspecs))); - }; - /** Pushes the given target branches upstream. */ - MergeStrategy.prototype.pushTargetBranchesUpstream = function (names) { - var _this = this; - var pushRefspecs = names.map(function (targetBranch) { - var localTargetBranch = _this.getLocalTargetBranchName(targetBranch); - return localTargetBranch + ":refs/heads/" + targetBranch; - }); - // Push all target branches with a single command if we don't run in dry-run mode. - // We don't want to push them individually as that could cause an unnecessary slow-down. - this.git.run(tslib.__spreadArray(['push', this.git.getRepoGitUrl()], tslib.__read(pushRefspecs))); - }; - return MergeStrategy; -}()); - -/** - * @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 - */ -/** Separator between commit message header and body. */ -var COMMIT_HEADER_SEPARATOR = '\n\n'; -/** - * Merge strategy that primarily leverages the Github API. The strategy merges a given - * pull request into a target branch using the API. This ensures that Github displays - * the pull request as merged. The merged commits are then cherry-picked into the remaining - * target branches using the local Git instance. The benefit is that the Github merged state - * is properly set, but a notable downside is that PRs cannot use fixup or squash commits. - */ -var GithubApiMergeStrategy = /** @class */ (function (_super) { - tslib.__extends(GithubApiMergeStrategy, _super); - function GithubApiMergeStrategy(git, _config) { - var _this = _super.call(this, git) || this; - _this._config = _config; - return _this; - } - GithubApiMergeStrategy.prototype.merge = function (pullRequest) { - return tslib.__awaiter(this, void 0, void 0, function () { - var githubTargetBranch, prNumber, targetBranches, requiredBaseSha, needsCommitMessageFixup, method, cherryPickTargetBranches, failure, mergeOptions, mergeStatusCode, targetSha, result, e_1, targetCommitsCount, failedBranches; - return tslib.__generator(this, function (_a) { - switch (_a.label) { - case 0: - githubTargetBranch = pullRequest.githubTargetBranch, prNumber = pullRequest.prNumber, targetBranches = pullRequest.targetBranches, requiredBaseSha = pullRequest.requiredBaseSha, needsCommitMessageFixup = pullRequest.needsCommitMessageFixup; - // If the pull request does not have its base branch set to any determined target - // branch, we cannot merge using the API. - if (targetBranches.every(function (t) { return t !== githubTargetBranch; })) { - return [2 /*return*/, PullRequestFailure.mismatchingTargetBranch(targetBranches)]; - } - // In cases where a required base commit is specified for this pull request, check if - // the pull request contains the given commit. If not, return a pull request failure. - // This check is useful for enforcing that PRs are rebased on top of a given commit. - // e.g. a commit that changes the code ownership validation. PRs which are not rebased - // could bypass new codeowner ship rules. - if (requiredBaseSha && !this.git.hasCommit(TEMP_PR_HEAD_BRANCH, requiredBaseSha)) { - return [2 /*return*/, PullRequestFailure.unsatisfiedBaseSha()]; - } - method = this._getMergeActionFromPullRequest(pullRequest); - cherryPickTargetBranches = targetBranches.filter(function (b) { return b !== githubTargetBranch; }); - return [4 /*yield*/, this._checkMergability(pullRequest, cherryPickTargetBranches)]; - case 1: - failure = _a.sent(); - // If the PR could not be cherry-picked into all target branches locally, we know it can't - // be done through the Github API either. We abort merging and pass-through the failure. - if (failure !== null) { - return [2 /*return*/, failure]; - } - mergeOptions = tslib.__assign({ pull_number: prNumber, merge_method: method }, this.git.remoteParams); - if (!needsCommitMessageFixup) return [3 /*break*/, 3]; - // Commit message fixup does not work with other merge methods as the Github API only - // allows commit message modifications for squash merging. - if (method !== 'squash') { - return [2 /*return*/, PullRequestFailure.unableToFixupCommitMessageSquashOnly()]; - } - return [4 /*yield*/, this._promptCommitMessageEdit(pullRequest, mergeOptions)]; - case 2: - _a.sent(); - _a.label = 3; - case 3: - _a.trys.push([3, 5, , 6]); - return [4 /*yield*/, this.git.github.pulls.merge(mergeOptions)]; - case 4: - result = _a.sent(); - mergeStatusCode = result.status; - targetSha = result.data.sha; - return [3 /*break*/, 6]; - case 5: - e_1 = _a.sent(); - // Note: Github usually returns `404` as status code if the API request uses a - // token with insufficient permissions. Github does this because it doesn't want - // to leak whether a repository exists or not. In our case we expect a certain - // repository to exist, so we always treat this as a permission failure. - if (e_1.status === 403 || e_1.status === 404) { - return [2 /*return*/, PullRequestFailure.insufficientPermissionsToMerge()]; - } - throw e_1; - case 6: - // https://developer.github.com/v3/pulls/#response-if-merge-cannot-be-performed - // Pull request cannot be merged due to merge conflicts. - if (mergeStatusCode === 405) { - return [2 /*return*/, PullRequestFailure.mergeConflicts([githubTargetBranch])]; - } - if (mergeStatusCode !== 200) { - return [2 /*return*/, PullRequestFailure.unknownMergeError()]; - } - // If the PR does not need to be merged into any other target branches, - // we exit here as we already completed the merge. - if (!cherryPickTargetBranches.length) { - return [2 /*return*/, null]; - } - // Refresh the target branch the PR has been merged into through the API. We need - // to re-fetch as otherwise we cannot cherry-pick the new commits into the remaining - // target branches. - this.fetchTargetBranches([githubTargetBranch]); - targetCommitsCount = method === 'squash' ? 1 : pullRequest.commitCount; - return [4 /*yield*/, this.cherryPickIntoTargetBranches(targetSha + "~" + targetCommitsCount + ".." + targetSha, cherryPickTargetBranches, { - // Commits that have been created by the Github API do not necessarily contain - // a reference to the source pull request (unless the squash strategy is used). - // To ensure that original commits can be found when a commit is viewed in a - // target branch, we add a link to the original commits when cherry-picking. - linkToOriginalCommits: true, - })]; - case 7: - failedBranches = _a.sent(); - // We already checked whether the PR can be cherry-picked into the target branches, - // but in case the cherry-pick somehow fails, we still handle the conflicts here. The - // commits created through the Github API could be different (i.e. through squash). - if (failedBranches.length) { - return [2 /*return*/, PullRequestFailure.mergeConflicts(failedBranches)]; - } - this.pushTargetBranchesUpstream(cherryPickTargetBranches); - return [2 /*return*/, null]; - } - }); - }); - }; - /** - * Prompts the user for the commit message changes. Unlike as in the autosquash merge - * strategy, we cannot start an interactive rebase because we merge using the Github API. - * The Github API only allows modifications to PR title and body for squash merges. - */ - GithubApiMergeStrategy.prototype._promptCommitMessageEdit = function (pullRequest, mergeOptions) { - return tslib.__awaiter(this, void 0, void 0, function () { - var commitMessage, result, _a, newTitle, newMessage; - return tslib.__generator(this, function (_b) { - switch (_b.label) { - case 0: return [4 /*yield*/, this._getDefaultSquashCommitMessage(pullRequest)]; - case 1: - commitMessage = _b.sent(); - return [4 /*yield*/, inquirer.prompt({ - type: 'editor', - name: 'result', - message: 'Please update the commit message', - default: commitMessage, - })]; - case 2: - result = (_b.sent()).result; - _a = tslib.__read(result.split(COMMIT_HEADER_SEPARATOR)), newTitle = _a[0], newMessage = _a.slice(1); - // Update the merge options so that the changes are reflected in there. - mergeOptions.commit_title = newTitle + " (#" + pullRequest.prNumber + ")"; - mergeOptions.commit_message = newMessage.join(COMMIT_HEADER_SEPARATOR); - return [2 /*return*/]; - } - }); - }); - }; - /** - * Gets a commit message for the given pull request. Github by default concatenates - * multiple commit messages if a PR is merged in squash mode. We try to replicate this - * behavior here so that we have a default commit message that can be fixed up. - */ - GithubApiMergeStrategy.prototype._getDefaultSquashCommitMessage = function (pullRequest) { - return tslib.__awaiter(this, void 0, void 0, function () { - var commits, messageBase, joinedMessages; - return tslib.__generator(this, function (_a) { - switch (_a.label) { - case 0: return [4 /*yield*/, this._getPullRequestCommitMessages(pullRequest)]; - case 1: - commits = (_a.sent()) - .map(function (message) { return ({ message: message, parsed: parseCommitMessage(message) }); }); - messageBase = "" + pullRequest.title + COMMIT_HEADER_SEPARATOR; - if (commits.length <= 1) { - return [2 /*return*/, "" + messageBase + commits[0].parsed.body]; - } - joinedMessages = commits.map(function (c) { return "* " + c.message; }).join(COMMIT_HEADER_SEPARATOR); - return [2 /*return*/, "" + messageBase + joinedMessages]; - } - }); - }); - }; - /** Gets all commit messages of commits in the pull request. */ - GithubApiMergeStrategy.prototype._getPullRequestCommitMessages = function (_a) { - var prNumber = _a.prNumber; - return tslib.__awaiter(this, void 0, void 0, function () { - var allCommits; - return tslib.__generator(this, function (_b) { - switch (_b.label) { - case 0: return [4 /*yield*/, this.git.github.paginate(this.git.github.pulls.listCommits, tslib.__assign(tslib.__assign({}, this.git.remoteParams), { pull_number: prNumber }))]; - case 1: - allCommits = _b.sent(); - return [2 /*return*/, allCommits.map(function (_a) { - var commit = _a.commit; - return commit.message; - })]; - } - }); - }); - }; - /** - * Checks if given pull request could be merged into its target branches. - * @returns A pull request failure if it the PR could not be merged. - */ - GithubApiMergeStrategy.prototype._checkMergability = function (pullRequest, targetBranches) { - return tslib.__awaiter(this, void 0, void 0, function () { - var revisionRange, failedBranches; - return tslib.__generator(this, function (_a) { - revisionRange = this.getPullRequestRevisionRange(pullRequest); - failedBranches = this.cherryPickIntoTargetBranches(revisionRange, targetBranches, { dryRun: true }); - if (failedBranches.length) { - return [2 /*return*/, PullRequestFailure.mergeConflicts(failedBranches)]; - } - return [2 /*return*/, null]; - }); - }); - }; - /** Determines the merge action from the given pull request. */ - GithubApiMergeStrategy.prototype._getMergeActionFromPullRequest = function (_a) { - var labels = _a.labels; - if (this._config.labels) { - var matchingLabel = this._config.labels.find(function (_a) { - var pattern = _a.pattern; - return labels.some(function (l) { return matchesPattern(l, pattern); }); - }); - if (matchingLabel !== undefined) { - return matchingLabel.method; - } - } - return this._config.default; - }; - return GithubApiMergeStrategy; -}(MergeStrategy)); - -/** - * @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 - */ -/** Path to the commit message filter script. Git expects this paths to use forward slashes. */ -var MSG_FILTER_SCRIPT = path.join(__dirname, './commit-message-filter.js').replace(/\\/g, '/'); -/** - * Merge strategy that does not use the Github API for merging. Instead, it fetches - * all target branches and the PR locally. The PR is then cherry-picked with autosquash - * enabled into the target branches. The benefit is the support for fixup and squash commits. - * A notable downside though is that Github does not show the PR as `Merged` due to non - * fast-forward merges - */ -var AutosquashMergeStrategy = /** @class */ (function (_super) { - tslib.__extends(AutosquashMergeStrategy, _super); - function AutosquashMergeStrategy() { - return _super !== null && _super.apply(this, arguments) || this; - } - /** - * Merges the specified pull request into the target branches and pushes the target - * branches upstream. This method requires the temporary target branches to be fetched - * already as we don't want to fetch the target branches per pull request merge. This - * would causes unnecessary multiple fetch requests when multiple PRs are merged. - * @throws {GitCommandError} An unknown Git command error occurred that is not - * specific to the pull request merge. - * @returns A pull request failure or null in case of success. - */ - AutosquashMergeStrategy.prototype.merge = function (pullRequest) { - return tslib.__awaiter(this, void 0, void 0, function () { - var prNumber, targetBranches, requiredBaseSha, needsCommitMessageFixup, githubTargetBranch, baseSha, revisionRange, branchOrRevisionBeforeRebase, rebaseEnv, failedBranches, localBranch, sha; - return tslib.__generator(this, function (_a) { - switch (_a.label) { - case 0: - prNumber = pullRequest.prNumber, targetBranches = pullRequest.targetBranches, requiredBaseSha = pullRequest.requiredBaseSha, needsCommitMessageFixup = pullRequest.needsCommitMessageFixup, githubTargetBranch = pullRequest.githubTargetBranch; - // In case a required base is specified for this pull request, check if the pull - // request contains the given commit. If not, return a pull request failure. This - // check is useful for enforcing that PRs are rebased on top of a given commit. e.g. - // a commit that changes the codeowner ship validation. PRs which are not rebased - // could bypass new codeowner ship rules. - if (requiredBaseSha && !this.git.hasCommit(TEMP_PR_HEAD_BRANCH, requiredBaseSha)) { - return [2 /*return*/, PullRequestFailure.unsatisfiedBaseSha()]; - } - baseSha = this.git.run(['rev-parse', this.getPullRequestBaseRevision(pullRequest)]).stdout.trim(); - revisionRange = baseSha + ".." + TEMP_PR_HEAD_BRANCH; - branchOrRevisionBeforeRebase = this.git.getCurrentBranchOrRevision(); - rebaseEnv = needsCommitMessageFixup ? undefined : tslib.__assign(tslib.__assign({}, process.env), { GIT_SEQUENCE_EDITOR: 'true' }); - this.git.run(['rebase', '--interactive', '--autosquash', baseSha, TEMP_PR_HEAD_BRANCH], { stdio: 'inherit', env: rebaseEnv }); - // Update pull requests commits to reference the pull request. This matches what - // Github does when pull requests are merged through the Web UI. The motivation is - // that it should be easy to determine which pull request contained a given commit. - // Note: The filter-branch command relies on the working tree, so we want to make sure - // that we are on the initial branch or revision where the merge script has been invoked. - this.git.run(['checkout', '-f', branchOrRevisionBeforeRebase]); - this.git.run(['filter-branch', '-f', '--msg-filter', MSG_FILTER_SCRIPT + " " + prNumber, revisionRange]); - failedBranches = this.cherryPickIntoTargetBranches(revisionRange, targetBranches); - if (failedBranches.length) { - return [2 /*return*/, PullRequestFailure.mergeConflicts(failedBranches)]; - } - this.pushTargetBranchesUpstream(targetBranches); - if (!(githubTargetBranch !== 'master')) return [3 /*break*/, 3]; - localBranch = this.getLocalTargetBranchName(githubTargetBranch); - sha = this.git.run(['rev-parse', localBranch]).stdout.trim(); - // Create a comment saying the PR was closed by the SHA. - return [4 /*yield*/, this.git.github.issues.createComment(tslib.__assign(tslib.__assign({}, this.git.remoteParams), { issue_number: pullRequest.prNumber, body: "Closed by commit " + sha }))]; - case 1: - // Create a comment saying the PR was closed by the SHA. - _a.sent(); - // Actually close the PR. - return [4 /*yield*/, this.git.github.pulls.update(tslib.__assign(tslib.__assign({}, this.git.remoteParams), { pull_number: pullRequest.prNumber, state: 'closed' }))]; - case 2: - // Actually close the PR. - _a.sent(); - _a.label = 3; - case 3: return [2 /*return*/, null]; - } - }); - }); - }; - return AutosquashMergeStrategy; -}(MergeStrategy)); - -/** - * @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 - */ -var defaultPullRequestMergeTaskFlags = { - branchPrompt: true, -}; -/** - * Class that accepts a merge script configuration and Github token. It provides - * a programmatic interface for merging multiple pull requests based on their - * labels that have been resolved through the merge script configuration. - */ -var PullRequestMergeTask = /** @class */ (function () { - function PullRequestMergeTask(config, git, flags) { - this.config = config; - this.git = git; - // Update flags property with the provided flags values as patches to the default flag values. - this.flags = tslib.__assign(tslib.__assign({}, defaultPullRequestMergeTaskFlags), flags); - } - /** - * Merges the given pull request and pushes it upstream. - * @param prNumber Pull request that should be merged. - * @param force Whether non-critical pull request failures should be ignored. - */ - PullRequestMergeTask.prototype.merge = function (prNumber, force) { - if (force === void 0) { force = false; } - return tslib.__awaiter(this, void 0, void 0, function () { - var hasOauthScopes, pullRequest, _a, _b, strategy, previousBranchOrRevision, failure, e_1; - var _this = this; - return tslib.__generator(this, function (_c) { - switch (_c.label) { - case 0: return [4 /*yield*/, this.git.hasOauthScopes(function (scopes, missing) { - if (!scopes.includes('repo')) { - if (_this.config.remote.private) { - missing.push('repo'); - } - else if (!scopes.includes('public_repo')) { - missing.push('public_repo'); - } - } - // Pull requests can modify Github action workflow files. In such cases Github requires us to - // push with a token that has the `workflow` oauth scope set. To avoid errors when the - // caretaker intends to merge such PRs, we ensure the scope is always set on the token before - // the merge process starts. - // https://docs.github.com/en/developers/apps/scopes-for-oauth-apps#available-scopes - if (!scopes.includes('workflow')) { - missing.push('workflow'); - } - })]; - case 1: - hasOauthScopes = _c.sent(); - if (hasOauthScopes !== true) { - return [2 /*return*/, { - status: 5 /* GITHUB_ERROR */, - failure: PullRequestFailure.insufficientPermissionsToMerge(hasOauthScopes.error) - }]; - } - if (this.git.hasUncommittedChanges()) { - return [2 /*return*/, { status: 1 /* DIRTY_WORKING_DIR */ }]; - } - return [4 /*yield*/, loadAndValidatePullRequest(this, prNumber, force)]; - case 2: - pullRequest = _c.sent(); - if (!isPullRequest(pullRequest)) { - return [2 /*return*/, { status: 3 /* FAILED */, failure: pullRequest }]; - } - _a = this.flags.branchPrompt; - if (!_a) return [3 /*break*/, 4]; - return [4 /*yield*/, promptConfirm(getTargettedBranchesConfirmationPromptMessage(pullRequest))]; - case 3: - _a = !(_c.sent()); - _c.label = 4; - case 4: - if (_a) { - return [2 /*return*/, { status: 4 /* USER_ABORTED */ }]; - } - _b = pullRequest.hasCaretakerNote; - if (!_b) return [3 /*break*/, 6]; - return [4 /*yield*/, promptConfirm(getCaretakerNotePromptMessage(pullRequest))]; - case 5: - _b = !(_c.sent()); - _c.label = 6; - case 6: - // If the pull request has a caretaker note applied, raise awareness by prompting - // the caretaker. The caretaker can then decide to proceed or abort the merge. - if (_b) { - return [2 /*return*/, { status: 4 /* USER_ABORTED */ }]; - } - strategy = this.config.githubApiMerge ? - new GithubApiMergeStrategy(this.git, this.config.githubApiMerge) : - new AutosquashMergeStrategy(this.git); - previousBranchOrRevision = null; - _c.label = 7; - case 7: - _c.trys.push([7, 11, 12, 13]); - previousBranchOrRevision = this.git.getCurrentBranchOrRevision(); - // Run preparations for the merge (e.g. fetching branches). - return [4 /*yield*/, strategy.prepare(pullRequest)]; - case 8: - // Run preparations for the merge (e.g. fetching branches). - _c.sent(); - return [4 /*yield*/, strategy.merge(pullRequest)]; - case 9: - failure = _c.sent(); - if (failure !== null) { - return [2 /*return*/, { status: 3 /* FAILED */, failure: failure }]; - } - // Switch back to the previous branch. We need to do this before deleting the temporary - // branches because we cannot delete branches which are currently checked out. - this.git.run(['checkout', '-f', previousBranchOrRevision]); - return [4 /*yield*/, strategy.cleanup(pullRequest)]; - case 10: - _c.sent(); - // Return a successful merge status. - return [2 /*return*/, { status: 2 /* SUCCESS */ }]; - case 11: - e_1 = _c.sent(); - // Catch all git command errors and return a merge result w/ git error status code. - // Other unknown errors which aren't caused by a git command are re-thrown. - if (e_1 instanceof GitCommandError) { - return [2 /*return*/, { status: 0 /* UNKNOWN_GIT_ERROR */ }]; - } - throw e_1; - case 12: - // Always try to restore the branch if possible. We don't want to leave - // the repository in a different state than before. - if (previousBranchOrRevision !== null) { - this.git.runGraceful(['checkout', '-f', previousBranchOrRevision]); - } - return [7 /*endfinally*/]; - case 13: return [2 /*return*/]; - } - }); - }); - }; - return PullRequestMergeTask; -}()); - -/** - * @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 - */ -/** - * Merges a given pull request based on labels configured in the given merge configuration. - * Pull requests can be merged with different strategies such as the Github API merge - * strategy, or the local autosquash strategy. Either strategy has benefits and downsides. - * More information on these strategies can be found in their dedicated strategy classes. - * - * See {@link GithubApiMergeStrategy} and {@link AutosquashMergeStrategy} - * - * @param prNumber Number of the pull request that should be merged. - * @param flags Configuration options for merging pull requests. - */ -function mergePullRequest(prNumber, flags) { - return tslib.__awaiter(this, void 0, void 0, function () { - /** Performs the merge and returns whether it was successful or not. */ - function performMerge(ignoreFatalErrors) { - return tslib.__awaiter(this, void 0, void 0, function () { - var result, e_1; - return tslib.__generator(this, function (_a) { - switch (_a.label) { - case 0: - _a.trys.push([0, 3, , 4]); - return [4 /*yield*/, api.merge(prNumber, ignoreFatalErrors)]; - case 1: - result = _a.sent(); - return [4 /*yield*/, handleMergeResult(result, ignoreFatalErrors)]; - case 2: return [2 /*return*/, _a.sent()]; - case 3: - e_1 = _a.sent(); - // Catch errors to the Github API for invalid requests. We want to - // exit the script with a better explanation of the error. - if (e_1 instanceof GithubApiRequestError && e_1.status === 401) { - error(red('Github API request failed. ' + e_1.message)); - error(yellow('Please ensure that your provided token is valid.')); - error(yellow("You can generate a token here: " + GITHUB_TOKEN_GENERATE_URL)); - process.exit(1); - } - throw e_1; - case 4: return [2 /*return*/]; - } - }); - }); - } - /** - * Prompts whether the specified pull request should be forcibly merged. If so, merges - * the specified pull request forcibly (ignoring non-critical failures). - * @returns Whether the specified pull request has been forcibly merged. - */ - function promptAndPerformForceMerge() { - return tslib.__awaiter(this, void 0, void 0, function () { - return tslib.__generator(this, function (_a) { - switch (_a.label) { - case 0: return [4 /*yield*/, promptConfirm('Do you want to forcibly proceed with merging?')]; - case 1: - if (_a.sent()) { - // Perform the merge in force mode. This means that non-fatal failures - // are ignored and the merge continues. - return [2 /*return*/, performMerge(true)]; - } - return [2 /*return*/, false]; - } - }); - }); - } - /** - * Handles the merge result by printing console messages, exiting the process - * based on the result, or by restarting the merge if force mode has been enabled. - * @returns Whether the merge completed without errors or not. - */ - function handleMergeResult(result, disableForceMergePrompt) { - if (disableForceMergePrompt === void 0) { disableForceMergePrompt = false; } - return tslib.__awaiter(this, void 0, void 0, function () { - var failure, status, canForciblyMerge, _a; - return tslib.__generator(this, function (_b) { - switch (_b.label) { - case 0: - failure = result.failure, status = result.status; - canForciblyMerge = failure && failure.nonFatal; - _a = status; - switch (_a) { - case 2 /* SUCCESS */: return [3 /*break*/, 1]; - case 1 /* DIRTY_WORKING_DIR */: return [3 /*break*/, 2]; - case 0 /* UNKNOWN_GIT_ERROR */: return [3 /*break*/, 3]; - case 5 /* GITHUB_ERROR */: return [3 /*break*/, 4]; - case 4 /* USER_ABORTED */: return [3 /*break*/, 5]; - case 3 /* FAILED */: return [3 /*break*/, 6]; - } - return [3 /*break*/, 9]; - case 1: - info(green("Successfully merged the pull request: #" + prNumber)); - return [2 /*return*/, true]; - case 2: - error(red("Local working repository not clean. Please make sure there are " + - "no uncommitted changes.")); - return [2 /*return*/, false]; - case 3: - error(red('An unknown Git error has been thrown. Please check the output ' + - 'above for details.')); - return [2 /*return*/, false]; - case 4: - error(red('An error related to interacting with Github has been discovered.')); - error(failure.message); - return [2 /*return*/, false]; - case 5: - info("Merge of pull request has been aborted manually: #" + prNumber); - return [2 /*return*/, true]; - case 6: - error(yellow("Could not merge the specified pull request.")); - error(red(failure.message)); - if (!(canForciblyMerge && !disableForceMergePrompt)) return [3 /*break*/, 8]; - info(); - info(yellow('The pull request above failed due to non-critical errors.')); - info(yellow("This error can be forcibly ignored if desired.")); - return [4 /*yield*/, promptAndPerformForceMerge()]; - case 7: return [2 /*return*/, _b.sent()]; - case 8: return [2 /*return*/, false]; - case 9: throw Error("Unexpected merge result: " + status); - } - }); - }); - } - var api; - return tslib.__generator(this, function (_a) { - switch (_a.label) { - case 0: - // Set the environment variable to skip all git commit hooks triggered by husky. We are unable to - // rely on `--no-verify` as some hooks still run, notably the `prepare-commit-msg` hook. - process.env['HUSKY'] = '0'; - return [4 /*yield*/, createPullRequestMergeTask(flags)]; - case 1: - api = _a.sent(); - return [4 /*yield*/, performMerge(false)]; - case 2: - // Perform the merge. Force mode can be activated through a command line flag. - // Alternatively, if the merge fails with non-fatal failures, the script - // will prompt whether it should rerun in force mode. - if (!(_a.sent())) { - process.exit(1); - } - return [2 /*return*/]; - } - }); - }); -} -/** - * Creates the pull request merge task using the given configuration options. Explicit configuration - * options can be specified when the merge script is used outside of an `ng-dev` configured - * repository. - */ -function createPullRequestMergeTask(flags) { - return tslib.__awaiter(this, void 0, void 0, function () { - var devInfraConfig, git, _a, config, errors; - return tslib.__generator(this, function (_b) { - switch (_b.label) { - case 0: - devInfraConfig = getConfig(); - git = AuthenticatedGitClient.get(); - return [4 /*yield*/, loadAndValidateConfig(devInfraConfig, git.github)]; - case 1: - _a = _b.sent(), config = _a.config, errors = _a.errors; - if (errors) { - error(red('Invalid merge configuration:')); - errors.forEach(function (desc) { return error(yellow(" - " + desc)); }); - process.exit(1); - } - // Set the remote so that the merge tool has access to information about - // the remote it intends to merge to. - config.remote = devInfraConfig.github; - // We can cast this to a merge config with remote because we always set the - // remote above. - return [2 /*return*/, new PullRequestMergeTask(config, git, flags)]; - } - }); - }); -} - -/** - * @license - * Copyright Google LLC All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ -/** Builds the command. */ -function builder$7(yargs) { - return addGithubTokenOption(yargs) - .help() - .strict() - .positional('pr', { - demandOption: true, - type: 'number', - description: 'The PR to be merged.', - }) - .option('branch-prompt', { - type: 'boolean', - default: true, - description: 'Whether to prompt to confirm the branches a PR will merge into.', - }); -} -/** Handles the command. */ -function handler$7(_a) { - var pr = _a.pr, branchPrompt = _a.branchPrompt; - return tslib.__awaiter(this, void 0, void 0, function () { - return tslib.__generator(this, function (_b) { - switch (_b.label) { - case 0: return [4 /*yield*/, mergePullRequest(pr, { branchPrompt: branchPrompt })]; - case 1: - _b.sent(); - return [2 /*return*/]; - } - }); - }); -} -/** yargs command module describing the command. */ -var MergeCommandModule = { - handler: handler$7, - builder: builder$7, - command: 'merge ', - describe: 'Merge a PR into its targeted branches.', -}; - -/** - * @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 - */ -/* Graphql schema for the response body for each pending PR. */ -const PR_SCHEMA$3 = { - state: typedGraphqlify.types.string, - maintainerCanModify: typedGraphqlify.types.boolean, - viewerDidAuthor: typedGraphqlify.types.boolean, - headRefOid: typedGraphqlify.types.string, - headRef: { - name: typedGraphqlify.types.string, - repository: { - url: typedGraphqlify.types.string, - nameWithOwner: typedGraphqlify.types.string, - }, - }, - baseRef: { - name: typedGraphqlify.types.string, - repository: { - url: typedGraphqlify.types.string, - nameWithOwner: typedGraphqlify.types.string, - }, - }, -}; -/** - * Rebase the provided PR onto its merge target branch, and push up the resulting - * commit to the PRs repository. - */ -function rebasePr(prNumber, githubToken, config = getConfig()) { - return tslib.__awaiter(this, void 0, void 0, function* () { - /** The singleton instance of the authenticated git client. */ - const git = AuthenticatedGitClient.get(); - if (git.hasUncommittedChanges()) { - error('Cannot perform rebase of PR with local changes.'); - process.exit(1); - } - /** - * The branch or revision originally checked out before this method performed - * any Git operations that may change the working branch. - */ - const previousBranchOrRevision = git.getCurrentBranchOrRevision(); - /* Get the PR information from Github. */ - const pr = yield getPr(PR_SCHEMA$3, prNumber, git); - const headRefName = pr.headRef.name; - const baseRefName = pr.baseRef.name; - const fullHeadRef = `${pr.headRef.repository.nameWithOwner}:${headRefName}`; - const fullBaseRef = `${pr.baseRef.repository.nameWithOwner}:${baseRefName}`; - const headRefUrl = addTokenToGitHttpsUrl(pr.headRef.repository.url, githubToken); - const baseRefUrl = addTokenToGitHttpsUrl(pr.baseRef.repository.url, githubToken); - // Note: Since we use a detached head for rebasing the PR and therefore do not have - // remote-tracking branches configured, we need to set our expected ref and SHA. This - // allows us to use `--force-with-lease` for the detached head while ensuring that we - // never accidentally override upstream changes that have been pushed in the meanwhile. - // See: - // https://git-scm.com/docs/git-push#Documentation/git-push.txt---force-with-leaseltrefnamegtltexpectgt - const forceWithLeaseFlag = `--force-with-lease=${headRefName}:${pr.headRefOid}`; - // If the PR does not allow maintainers to modify it, exit as the rebased PR cannot - // be pushed up. - if (!pr.maintainerCanModify && !pr.viewerDidAuthor) { - error(`Cannot rebase as you did not author the PR and the PR does not allow maintainers` + - `to modify the PR`); - process.exit(1); - } - try { - // Fetch the branch at the commit of the PR, and check it out in a detached state. - info(`Checking out PR #${prNumber} from ${fullHeadRef}`); - git.run(['fetch', '-q', headRefUrl, headRefName]); - git.run(['checkout', '-q', '--detach', 'FETCH_HEAD']); - // Fetch the PRs target branch and rebase onto it. - info(`Fetching ${fullBaseRef} to rebase #${prNumber} on`); - git.run(['fetch', '-q', baseRefUrl, baseRefName]); - const commonAncestorSha = git.run(['merge-base', 'HEAD', 'FETCH_HEAD']).stdout.trim(); - const commits = yield getCommitsInRange(commonAncestorSha, 'HEAD'); - let squashFixups = commits.filter((commit) => commit.isFixup).length === 0 ? - false : - yield promptConfirm(`PR #${prNumber} contains fixup commits, would you like to squash them during rebase?`, true); - info(`Attempting to rebase PR #${prNumber} on ${fullBaseRef}`); - /** - * Tuple of flags to be added to the rebase command and env object to run the git command. - * - * Additional flags to perform the autosquashing are added when the user confirm squashing of - * fixup commits should occur. - */ - const [flags, env] = squashFixups ? - [['--interactive', '--autosquash'], Object.assign(Object.assign({}, process.env), { GIT_SEQUENCE_EDITOR: 'true' })] : - [[], undefined]; - const rebaseResult = git.runGraceful(['rebase', ...flags, 'FETCH_HEAD'], { env: env }); - // If the rebase was clean, push the rebased PR up to the authors fork. - if (rebaseResult.status === 0) { - info(`Rebase was able to complete automatically without conflicts`); - info(`Pushing rebased PR #${prNumber} to ${fullHeadRef}`); - git.run(['push', headRefUrl, `HEAD:${headRefName}`, forceWithLeaseFlag]); - info(`Rebased and updated PR #${prNumber}`); - git.checkout(previousBranchOrRevision, true); - process.exit(0); - } - } - catch (err) { - error(err.message); - git.checkout(previousBranchOrRevision, true); - process.exit(1); - } - // On automatic rebase failures, prompt to choose if the rebase should be continued - // manually or aborted now. - info(`Rebase was unable to complete automatically without conflicts.`); - // If the command is run in a non-CI environment, prompt to format the files immediately. - const continueRebase = process.env['CI'] === undefined && (yield promptConfirm('Manually complete rebase?')); - if (continueRebase) { - info(`After manually completing rebase, run the following command to update PR #${prNumber}:`); - info(` $ git push ${pr.headRef.repository.url} HEAD:${headRefName} ${forceWithLeaseFlag}`); - info(); - info(`To abort the rebase and return to the state of the repository before this command`); - info(`run the following command:`); - info(` $ git rebase --abort && git reset --hard && git checkout ${previousBranchOrRevision}`); - process.exit(1); - } - else { - info(`Cleaning up git state, and restoring previous state.`); - } - git.checkout(previousBranchOrRevision, true); - 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 - */ -/** Builds the rebase pull request command. */ -function buildRebaseCommand(yargs) { - return addGithubTokenOption(yargs).positional('prNumber', { type: 'number', demandOption: true }); -} -/** Handles the rebase pull request command. */ -function handleRebaseCommand({ prNumber, githubToken }) { - return tslib.__awaiter(this, void 0, void 0, function* () { - yield rebasePr(prNumber, githubToken); - }); -} - -/** - * @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 - */ -/** Build the parser for pull request commands. */ -function buildPrParser(localYargs) { - return localYargs.help() - .strict() - .demandCommand() - .command('discover-new-conflicts ', 'Check if a pending PR causes new conflicts for other pending PRs', buildDiscoverNewConflictsCommand, handleDiscoverNewConflictsCommand) - .command('rebase ', 'Rebase a pending PR and push the rebased commits back to Github', buildRebaseCommand, handleRebaseCommand) - .command(MergeCommandModule) - .command(CheckoutCommandModule) - .command(CheckTargetBranchesModule); -} - -/** - * @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 - */ -/** Create logs for each pullapprove group result. */ -function logGroup(group, conditionsToPrint, printMessageFn = info) { - const conditions = group[conditionsToPrint]; - printMessageFn.group(`[${group.groupName}]`); - if (conditions.length) { - conditions.forEach(groupCondition => { - const count = groupCondition.matchedFiles.size; - if (conditionsToPrint === 'unverifiableConditions') { - printMessageFn(`${groupCondition.expression}`); - } - else { - printMessageFn(`${count} ${count === 1 ? 'match' : 'matches'} - ${groupCondition.expression}`); - } - }); - printMessageFn.groupEnd(); - } -} -/** Logs a header within a text drawn box. */ -function logHeader(...params) { - const totalWidth = 80; - const fillWidth = totalWidth - 2; - const headerText = params.join(' ').substr(0, fillWidth); - const leftSpace = Math.ceil((fillWidth - headerText.length) / 2); - const rightSpace = fillWidth - leftSpace - headerText.length; - const fill = (count, content) => content.repeat(count); - info(`┌${fill(fillWidth, '─')}┐`); - info(`│${fill(leftSpace, ' ')}${headerText}${fill(rightSpace, ' ')}│`); - info(`└${fill(fillWidth, '─')}┘`); -} - -/** - * @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 - */ -/** Map that holds patterns and their corresponding Minimatch globs. */ -const patternCache = new Map(); -/** - * Gets a glob for the given pattern. The cached glob will be returned - * if available. Otherwise a new glob will be created and cached. - */ -function getOrCreateGlob(pattern) { - if (patternCache.has(pattern)) { - return patternCache.get(pattern); - } - const glob = new minimatch.Minimatch(pattern, { dot: true }); - patternCache.set(pattern, glob); - return glob; -} - -class PullApproveGroupStateDependencyError extends Error { - constructor(message) { - super(message); - // 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, PullApproveGroupStateDependencyError.prototype); - // Error names are displayed in their stack but can't be set in the constructor. - this.name = PullApproveGroupStateDependencyError.name; - } -} -/** - * Superset of a native array. The superset provides methods which mimic the - * list data structure used in PullApprove for files in conditions. - */ -class PullApproveStringArray extends Array { - constructor(...elements) { - super(...elements); - // 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, PullApproveStringArray.prototype); - } - /** Returns a new array which only includes files that match the given pattern. */ - include(pattern) { - return new PullApproveStringArray(...this.filter(s => getOrCreateGlob(pattern).match(s))); - } - /** Returns a new array which only includes files that did not match the given pattern. */ - exclude(pattern) { - return new PullApproveStringArray(...this.filter(s => !getOrCreateGlob(pattern).match(s))); - } -} -/** - * Superset of a native array. The superset provides methods which mimic the - * list data structure used in PullApprove for groups in conditions. - */ -class PullApproveGroupArray extends Array { - constructor(...elements) { - super(...elements); - // 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, PullApproveGroupArray.prototype); - } - include(pattern) { - return new PullApproveGroupArray(...this.filter(s => s.groupName.match(pattern))); - } - /** Returns a new array which only includes files that did not match the given pattern. */ - exclude(pattern) { - return new PullApproveGroupArray(...this.filter(s => s.groupName.match(pattern))); - } - get pending() { - throw new PullApproveGroupStateDependencyError(); - } - get active() { - throw new PullApproveGroupStateDependencyError(); - } - get inactive() { - throw new PullApproveGroupStateDependencyError(); - } - get rejected() { - throw new PullApproveGroupStateDependencyError(); - } - get names() { - return this.map(g => g.groupName); - } -} - -/** - * @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 - */ -/** - * Context that is provided to conditions. Conditions can use various helpers - * that PullApprove provides. We try to mock them here. Consult the official - * docs for more details: https://docs.pullapprove.com/config/conditions. - */ -const conditionContext = { - 'len': (value) => value.length, - 'contains_any_globs': (files, patterns) => { - // Note: Do not always create globs for the same pattern again. This method - // could be called for each source file. Creating glob's is expensive. - return files.some(f => patterns.some(pattern => getOrCreateGlob(pattern).match(f))); - }, -}; -/** - * Converts a given condition to a function that accepts a set of files. The returned - * function can be called to check if the set of files matches the condition. - */ -function convertConditionToFunction(expr) { - // Creates a dynamic function with the specified expression. - // The first parameter will be `files` as that corresponds to the supported `files` variable that - // can be accessed in PullApprove condition expressions. The second parameter is the list of - // PullApproveGroups that are accessible in the condition expressions. The followed parameters - // correspond to other context variables provided by PullApprove for conditions. - const evaluateFn = new Function('files', 'groups', ...Object.keys(conditionContext), ` - return (${transformExpressionToJs(expr)}); - `); - // Create a function that calls the dynamically constructed function which mimics - // the condition expression that is usually evaluated with Python in PullApprove. - return (files, groups) => { - const result = evaluateFn(new PullApproveStringArray(...files), new PullApproveGroupArray(...groups), ...Object.values(conditionContext)); - // If an array is returned, we consider the condition as active if the array is not - // empty. This matches PullApprove's condition evaluation that is based on Python. - if (Array.isArray(result)) { - return result.length !== 0; - } - return !!result; - }; -} -/** - * Transforms a condition expression from PullApprove that is based on python - * so that it can be run inside JavaScript. Current transformations: - * 1. `not <..>` -> `!<..>` - */ -function transformExpressionToJs(expression) { - return expression.replace(/not\s+/g, '!'); -} - -/** - * @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 - */ -// Regular expression that matches conditions for the global approval. -const GLOBAL_APPROVAL_CONDITION_REGEX = /^"global-(docs-)?approvers" not in groups.approved$/; -/** A PullApprove group to be able to test files against. */ -class PullApproveGroup { - constructor(groupName, config, precedingGroups = []) { - var _a; - this.groupName = groupName; - this.precedingGroups = precedingGroups; - /** List of conditions for the group. */ - this.conditions = []; - this._captureConditions(config); - this.reviewers = (_a = config.reviewers) !== null && _a !== void 0 ? _a : { users: [], teams: [] }; - } - _captureConditions(config) { - if (config.conditions) { - return config.conditions.forEach(condition => { - const expression = condition.trim(); - if (expression.match(GLOBAL_APPROVAL_CONDITION_REGEX)) { - // Currently a noop as we don't take any action for global approval conditions. - return; - } - try { - this.conditions.push({ - expression, - checkFn: convertConditionToFunction(expression), - matchedFiles: new Set(), - unverifiable: false, - }); - } - catch (e) { - error(`Could not parse condition in group: ${this.groupName}`); - error(` - ${expression}`); - error(`Error:`); - error(e.message); - error(e.stack); - process.exit(1); - } - }); - } - } - /** - * Tests a provided file path to determine if it would be considered matched by - * the pull approve group's conditions. - */ - testFile(filePath) { - return this.conditions.every((condition) => { - const { matchedFiles, checkFn, expression } = condition; - try { - const matchesFile = checkFn([filePath], this.precedingGroups); - if (matchesFile) { - matchedFiles.add(filePath); - } - return matchesFile; - } - catch (e) { - // In the case of a condition that depends on the state of groups we want to - // ignore that the verification can't accurately evaluate the condition and then - // continue processing. Other types of errors fail the verification, as conditions - // should otherwise be able to execute without throwing. - if (e instanceof PullApproveGroupStateDependencyError) { - condition.unverifiable = true; - // Return true so that `this.conditions.every` can continue evaluating. - return true; - } - else { - const errMessage = `Condition could not be evaluated: \n\n` + - `From the [${this.groupName}] group:\n` + - ` - ${expression}` + - `\n\n${e.message} ${e.stack}\n\n`; - error(errMessage); - process.exit(1); - } - } - }); - } - /** Retrieve the results for the Group, all matched and unmatched conditions. */ - getResults() { - const matchedConditions = this.conditions.filter(c => c.matchedFiles.size > 0); - const unmatchedConditions = this.conditions.filter(c => c.matchedFiles.size === 0 && !c.unverifiable); - const unverifiableConditions = this.conditions.filter(c => c.unverifiable); - return { - matchedConditions, - matchedCount: matchedConditions.length, - unmatchedConditions, - unmatchedCount: unmatchedConditions.length, - unverifiableConditions, - groupName: this.groupName, - }; - } -} - -/** - * @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 - */ -function parsePullApproveYaml(rawYaml) { - return yaml.parse(rawYaml, { merge: true }); -} -/** Parses all of the groups defined in the pullapprove yaml. */ -function getGroupsFromYaml(pullApproveYamlRaw) { - /** JSON representation of the pullapprove yaml file. */ - const pullApprove = parsePullApproveYaml(pullApproveYamlRaw); - return Object.entries(pullApprove.groups).reduce((groups, [groupName, group]) => { - return groups.concat(new PullApproveGroup(groupName, group, groups)); - }, []); -} - -/** - * @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 - */ -function verify$1() { - const git = GitClient.get(); - /** Full path to PullApprove config file */ - const PULL_APPROVE_YAML_PATH = path.resolve(git.baseDir, '.pullapprove.yml'); - /** All tracked files in the repository. */ - const REPO_FILES = git.allFiles(); - /** The pull approve config file. */ - const pullApproveYamlRaw = fs.readFileSync(PULL_APPROVE_YAML_PATH, 'utf8'); - /** All of the groups defined in the pullapprove yaml. */ - const groups = getGroupsFromYaml(pullApproveYamlRaw); - /** - * PullApprove groups without conditions. These are skipped in the verification - * as those would always be active and cause zero unmatched files. - */ - const groupsSkipped = groups.filter(group => !group.conditions.length); - /** PullApprove groups with conditions. */ - const groupsWithConditions = groups.filter(group => !!group.conditions.length); - /** Files which are matched by at least one group. */ - const matchedFiles = []; - /** Files which are not matched by at least one group. */ - const unmatchedFiles = []; - // Test each file in the repo against each group for being matched. - REPO_FILES.forEach((file) => { - if (groupsWithConditions.filter(group => group.testFile(file)).length) { - matchedFiles.push(file); - } - else { - unmatchedFiles.push(file); - } - }); - /** Results for each group */ - const resultsByGroup = groupsWithConditions.map(group => group.getResults()); - /** - * Whether all group condition lines match at least one file and all files - * are matched by at least one group. - */ - const allGroupConditionsValid = resultsByGroup.every(r => !r.unmatchedCount) && !unmatchedFiles.length; - /** Whether all groups have at least one reviewer user or team defined. */ - const groupsWithoutReviewers = groups.filter(group => Object.keys(group.reviewers).length === 0); - /** The overall result of the verifcation. */ - const overallResult = allGroupConditionsValid && groupsWithoutReviewers.length === 0; - /** - * Overall result - */ - logHeader('Overall Result'); - if (overallResult) { - info('PullApprove verification succeeded!'); - } - else { - info(`PullApprove verification failed.`); - info(); - info(`Please update '.pullapprove.yml' to ensure that all necessary`); - info(`files/directories have owners and all patterns that appear in`); - info(`the file correspond to actual files/directories in the repo.`); - } - /** Reviewers check */ - logHeader(`Group Reviewers Check`); - if (groupsWithoutReviewers.length === 0) { - info('All group contain at least one reviewer user or team.'); - } - else { - info.group(`Discovered ${groupsWithoutReviewers.length} group(s) without a reviewer defined`); - groupsWithoutReviewers.forEach(g => info(g.groupName)); - info.groupEnd(); - } - /** - * File by file Summary - */ - logHeader('PullApprove results by file'); - info.group(`Matched Files (${matchedFiles.length} files)`); - matchedFiles.forEach(file => debug(file)); - info.groupEnd(); - info.group(`Unmatched Files (${unmatchedFiles.length} files)`); - unmatchedFiles.forEach(file => info(file)); - info.groupEnd(); - /** - * Group by group Summary - */ - logHeader('PullApprove results by group'); - info.group(`Groups skipped (${groupsSkipped.length} groups)`); - groupsSkipped.forEach(group => debug(`${group.groupName}`)); - info.groupEnd(); - const matchedGroups = resultsByGroup.filter(group => !group.unmatchedCount); - info.group(`Matched conditions by Group (${matchedGroups.length} groups)`); - matchedGroups.forEach(group => logGroup(group, 'matchedConditions', debug)); - info.groupEnd(); - const unmatchedGroups = resultsByGroup.filter(group => group.unmatchedCount); - info.group(`Unmatched conditions by Group (${unmatchedGroups.length} groups)`); - unmatchedGroups.forEach(group => logGroup(group, 'unmatchedConditions')); - info.groupEnd(); - const unverifiableConditionsInGroups = resultsByGroup.filter(group => group.unverifiableConditions.length > 0); - info.group(`Unverifiable conditions by Group (${unverifiableConditionsInGroups.length} groups)`); - unverifiableConditionsInGroups.forEach(group => logGroup(group, 'unverifiableConditions')); - info.groupEnd(); - // Provide correct exit code based on verification success. - process.exit(overallResult ? 0 : 1); -} - -/** Build the parser for the pullapprove commands. */ -function buildPullapproveParser(localYargs) { - return localYargs.help().strict().demandCommand().command('verify', 'Verify the pullapprove config', {}, () => verify$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 - */ -/** - * Builds the release output without polluting the process stdout. Build scripts commonly - * print messages to stderr or stdout. This is fine in most cases, but sometimes other tooling - * reserves stdout for data transfer (e.g. when `ng release build --json` is invoked). To not - * pollute the stdout in such cases, we launch a child process for building the release packages - * and redirect all stdout output to the stderr channel (which can be read in the terminal). - */ -function buildReleaseOutput(stampForRelease = false) { - return tslib.__awaiter(this, void 0, void 0, function* () { - return new Promise(resolve => { - const buildProcess = child_process.fork(require.resolve('./build-worker'), [`${stampForRelease}`], { - // The stdio option is set to redirect any "stdout" output directly to the "stderr" file - // descriptor. An additional "ipc" file descriptor is created to support communication with - // the build process. https://nodejs.org/api/child_process.html#child_process_options_stdio. - stdio: ['inherit', 2, 2, 'ipc'], - }); - let builtPackages = null; - // The child process will pass the `buildPackages()` output through the - // IPC channel. We keep track of it so that we can use it as resolve value. - buildProcess.on('message', buildResponse => builtPackages = buildResponse); - // On child process exit, resolve the promise with the received output. - buildProcess.on('exit', () => resolve(builtPackages)); - }); - }); -} - -/** - * @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 - */ -/** Yargs command builder for configuring the `ng-dev release build` command. */ -function builder$8(argv) { - return argv.option('json', { - type: 'boolean', - description: 'Whether the built packages should be printed to stdout as JSON.', - default: false, - }); -} -/** Yargs command handler for building a release. */ -function handler$8(args) { - return tslib.__awaiter(this, void 0, void 0, function* () { - const { npmPackages } = getReleaseConfig(); - let builtPackages = yield buildReleaseOutput(true); - // If package building failed, print an error and exit with an error code. - if (builtPackages === null) { - error(red(` ✘ Could not build release output. Please check output above.`)); - process.exit(1); - } - // If no packages have been built, we assume that this is never correct - // and exit with an error code. - if (builtPackages.length === 0) { - error(red(` ✘ No release packages have been built. Please ensure that the`)); - error(red(` build script is configured correctly in ".ng-dev".`)); - process.exit(1); - } - const missingPackages = npmPackages.filter(pkgName => !builtPackages.find(b => b.name === pkgName)); - // Check for configured release packages which have not been built. We want to - // error and exit if any configured package has not been built. - if (missingPackages.length > 0) { - error(red(` ✘ Release output missing for the following packages:`)); - missingPackages.forEach(pkgName => error(red(` - ${pkgName}`))); - process.exit(1); - } - if (args.json) { - process.stdout.write(JSON.stringify(builtPackages, null, 2)); - } - else { - info(green(' ✓ Built release packages.')); - builtPackages.forEach(({ name }) => info(green(` - ${name}`))); - } - }); -} -/** CLI command module for building release output. */ -const ReleaseBuildCommandModule = { - builder: builder$8, - handler: handler$8, - command: 'build', - describe: 'Builds the release output for the current branch.', -}; - -/** - * @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 - */ -/** - * Prints the active release trains to the console. - * @params active Active release trains that should be printed. - * @params config Release configuration used for querying NPM on published versions. - */ -function printActiveReleaseTrains(active, config) { - return tslib.__awaiter(this, void 0, void 0, function* () { - const { releaseCandidate, next, latest } = active; - const isNextPublishedToNpm = yield isVersionPublishedToNpm(next.version, config); - const nextTrainType = next.isMajor ? 'major' : 'minor'; - const ltsBranches = yield fetchLongTermSupportBranchesFromNpm(config); - info(); - info(blue('Current version branches in the project:')); - // Print information for release trains in the feature-freeze/release-candidate phase. - if (releaseCandidate !== null) { - const rcVersion = releaseCandidate.version; - const rcTrainType = releaseCandidate.isMajor ? 'major' : 'minor'; - const rcTrainPhase = rcVersion.prerelease[0] === 'next' ? 'feature-freeze' : 'release-candidate'; - info(` • ${bold(releaseCandidate.branchName)} contains changes for an upcoming ` + - `${rcTrainType} that is currently in ${bold(rcTrainPhase)} phase.`); - info(` Most recent pre-release for this branch is "${bold(`v${rcVersion}`)}".`); - } - // Print information about the release-train in the latest phase. i.e. the patch branch. - info(` • ${bold(latest.branchName)} contains changes for the most recent patch.`); - info(` Most recent patch version for this branch is "${bold(`v${latest.version}`)}".`); - // Print information about the release-train in the next phase. - info(` • ${bold(next.branchName)} contains changes for a ${nextTrainType} ` + - `currently in active development.`); - // Note that there is a special case for versions in the next release-train. The version in - // the next branch is not always published to NPM. This can happen when we recently branched - // off for a feature-freeze release-train. More details are in the next pre-release action. - if (isNextPublishedToNpm) { - info(` Most recent pre-release version for this branch is "${bold(`v${next.version}`)}".`); - } - else { - info(` Version is currently set to "${bold(`v${next.version}`)}", but has not been ` + - `published yet.`); - } - // If no release-train in release-candidate or feature-freeze phase is active, - // we print a message as last bullet point to make this clear. - if (releaseCandidate === null) { - info(' • No release-candidate or feature-freeze branch currently active.'); - } - info(); - info(blue('Current active LTS version branches:')); - // Print all active LTS branches (each branch as own bullet point). - if (ltsBranches.active.length !== 0) { - for (const ltsBranch of ltsBranches.active) { - info(` • ${bold(ltsBranch.name)} is currently in active long-term support phase.`); - info(` Most recent patch version for this branch is "${bold(`v${ltsBranch.version}`)}".`); - } - } - info(); - }); -} - -/** - * @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 - */ -/** Yargs command handler for printing release information. */ -function handler$9() { - return tslib.__awaiter(this, void 0, void 0, function* () { - const git = GitClient.get(); - const gitRepoWithApi = Object.assign({ api: git.github }, git.remoteConfig); - const releaseTrains = yield fetchActiveReleaseTrains(gitRepoWithApi); - // Print the active release trains. - yield printActiveReleaseTrains(releaseTrains, getReleaseConfig()); - }); -} -/** CLI command module for retrieving release information. */ -const ReleaseInfoCommandModule = { - handler: handler$9, - command: 'info', - describe: 'Prints active release trains to the console.', -}; - -/** - * @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 - */ -/** List of types to be included in the release notes. */ -const typesToIncludeInReleaseNotes = Object.values(COMMIT_TYPES) - .filter(type => type.releaseNotesLevel === ReleaseNotesLevel.Visible) - .map(type => type.name); -/** Context class used for rendering release notes. */ -class RenderContext { - constructor(data) { - this.data = data; - /** An array of group names in sort order if defined. */ - this.groupOrder = this.data.groupOrder || []; - /** An array of scopes to hide from the release entry output. */ - this.hiddenScopes = this.data.hiddenScopes || []; - /** The title of the release, or `false` if no title should be used. */ - this.title = this.data.title; - /** An array of commits in the release period. */ - this.commits = this.data.commits; - /** The version of the release. */ - this.version = this.data.version; - /** The date stamp string for use in the release notes entry. */ - this.dateStamp = buildDateStamp(this.data.date); - } - /** - * Organizes and sorts the commits into groups of commits. - * - * Groups are sorted either by default `Array.sort` order, or using the provided group order from - * the configuration. Commits are order in the same order within each groups commit list as they - * appear in the provided list of commits. - * */ - asCommitGroups(commits) { - /** The discovered groups to organize into. */ - const groups = new Map(); - // Place each commit in the list into its group. - commits.forEach(commit => { - const key = commit.npmScope ? `${commit.npmScope}/${commit.scope}` : commit.scope; - const groupCommits = groups.get(key) || []; - groups.set(key, groupCommits); - groupCommits.push(commit); - }); - /** - * Array of CommitGroups containing the discovered commit groups. Sorted in alphanumeric order - * of the group title. - */ - const commitGroups = Array.from(groups.entries()) - .map(([title, commits]) => ({ title, commits })) - .sort((a, b) => a.title > b.title ? 1 : a.title < b.title ? -1 : 0); - // If the configuration provides a sorting order, updated the sorted list of group keys to - // satisfy the order of the groups provided in the list with any groups not found in the list at - // the end of the sorted list. - if (this.groupOrder.length) { - for (const groupTitle of this.groupOrder.reverse()) { - const currentIdx = commitGroups.findIndex(k => k.title === groupTitle); - if (currentIdx !== -1) { - const removedGroups = commitGroups.splice(currentIdx, 1); - commitGroups.splice(0, 0, ...removedGroups); - } - } - } - return commitGroups; - } - /** - * A filter function for filtering a list of commits to only include commits which should appear - * in release notes. - */ - includeInReleaseNotes() { - return (commit) => { - if (!typesToIncludeInReleaseNotes.includes(commit.type)) { - return false; - } - if (this.hiddenScopes.includes(commit.scope)) { - return false; - } - return true; - }; - } - /** - * A filter function for filtering a list of commits to only include commits which contain a - * truthy value, or for arrays an array with 1 or more elements, for the provided field. - */ - contains(field) { - return (commit) => { - const fieldValue = commit[field]; - if (!fieldValue) { - return false; - } - if (Array.isArray(fieldValue) && fieldValue.length === 0) { - return false; - } - return true; - }; - } - /** - * A filter function for filtering a list of commits to only include commits which contain a - * unique value for the provided field across all commits in the list. - */ - unique(field) { - const set = new Set(); - return (commit) => { - const include = !set.has(commit[field]); - set.add(commit[field]); - return include; - }; - } - /** - * Convert a commit object to a Markdown link. - */ - commitToLink(commit) { - const url = `https://github.com/${this.data.github.owner}/${this.data.github.name}/commit/${commit.hash}`; - return `[${commit.shortHash}](${url})`; - } - /** - * Convert a pull request number to a Markdown link. - */ - pullRequestToLink(prNumber) { - const url = `https://github.com/${this.data.github.owner}/${this.data.github.name}/pull/${prNumber}`; - return `[#${prNumber}](${url})`; - } - /** - * Transform a commit message header by replacing the parenthesized pull request reference at the - * end of the line (which is added by merge tooling) to a Markdown link. - */ - replaceCommitHeaderPullRequestNumber(header) { - return header.replace(/\(#(\d+)\)$/, (_, g) => `(${this.pullRequestToLink(+g)})`); - } -} -/** - * Builds a date stamp for stamping in release notes. - * - * Uses the current date, or a provided date in the format of YYYY-MM-DD, i.e. 1970-11-05. - */ -function buildDateStamp(date = new Date()) { - const year = `${date.getFullYear()}`; - const month = `${(date.getMonth() + 1)}`.padStart(2, '0'); - const day = `${date.getDate()}`.padStart(2, '0'); - return [year, month, day].join('-'); -} - -/** - * @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 - */ -var changelogTemplate = ` - -# <%- version %><% if (title) { %> "<%- title %>"<% } %> (<%- dateStamp %>) - -<%_ -const commitsInChangelog = commits.filter(includeInReleaseNotes()); -for (const group of asCommitGroups(commitsInChangelog)) { -_%> - -### <%- group.title %> -| Commit | Description | -| -- | -- | -<%_ - for (const commit of group.commits) { -_%> -| <%- commitToLink(commit) %> | <%- replaceCommitHeaderPullRequestNumber(commit.header) %> | -<%_ - } -} -_%> - -<%_ -const breakingChanges = commits.filter(contains('breakingChanges')); -if (breakingChanges.length) { -_%> -## Breaking Changes - -<%_ - for (const group of asCommitGroups(breakingChanges)) { -_%> -### <%- group.title %> - -<%_ - for (const commit of group.commits) { -_%> -<%- commit.breakingChanges[0].text %> - -<%_ - } - } -} -_%> - -<%_ -const deprecations = commits.filter(contains('deprecations')); -if (deprecations.length) { -_%> -## Deprecations -<%_ - for (const group of asCommitGroups(deprecations)) { -_%> -### <%- group.title %> - -<%_ - for (const commit of group.commits) { -_%> -<%- commit.deprecations[0].text %> -<%_ - } - } -} -_%> - -<%_ -const botsAuthorName = ['dependabot[bot]', 'Renovate Bot']; -const authors = commits - .filter(unique('author')) - .map(c => c.author) - .filter(a => !botsAuthorName.includes(a)) - .sort(); -if (authors.length === 1) { -_%> -## Special Thanks: -<%- authors[0]%> -<%_ -} -if (authors.length > 1) { -_%> -## Special Thanks: -<%- authors.slice(0, -1).join(', ') %> and <%- authors.slice(-1)[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 - */ -var githubReleaseTemplate = ` - -# <%- version %><% if (title) { %> "<%- title %>"<% } %> (<%- dateStamp %>) - -<%_ -const commitsInChangelog = commits.filter(includeInReleaseNotes()); -for (const group of asCommitGroups(commitsInChangelog)) { -_%> - -### <%- group.title %> -| Commit | Description | -| -- | -- | -<%_ - for (const commit of group.commits) { -_%> -| <%- commit.shortHash %> | <%- commit.header %> | -<%_ - } -} -_%> - -<%_ -const breakingChanges = commits.filter(contains('breakingChanges')); -if (breakingChanges.length) { -_%> -## Breaking Changes - -<%_ - for (const group of asCommitGroups(breakingChanges)) { -_%> -### <%- group.title %> - -<%_ - for (const commit of group.commits) { -_%> -<%- commit.breakingChanges[0].text %> - -<%_ - } - } -} -_%> - -<%_ -const deprecations = commits.filter(contains('deprecations')); -if (deprecations.length) { -_%> -## Deprecations -<%_ - for (const group of asCommitGroups(deprecations)) { -_%> -### <%- group.title %> - -<%_ - for (const commit of group.commits) { -_%> -<%- commit.deprecations[0].text %> -<%_ - } - } -} -_%> - -<%_ -const authors = commits.filter(unique('author')).map(c => c.author).sort(); -if (authors.length === 1) { -_%> -## Special Thanks: -<%- authors[0]%> -<%_ -} -if (authors.length > 1) { -_%> -## Special Thanks: -<%- authors.slice(0, -1).join(', ') %> and <%- authors.slice(-1)[0] %> -<%_ -} -_%> -`; - -/** Release note generation. */ -class ReleaseNotes { - constructor(version, startingRef, endingRef) { - this.version = version; - this.startingRef = startingRef; - this.endingRef = endingRef; - /** An instance of GitClient. */ - this.git = GitClient.get(); - /** A promise resolving to a list of Commits since the latest semver tag on the branch. */ - this.commits = this.getCommitsInRange(this.startingRef, this.endingRef); - /** The configuration for release notes. */ - this.config = this.getReleaseConfig().releaseNotes; - } - static fromRange(version, startingRef, endingRef) { - return tslib.__awaiter(this, void 0, void 0, function* () { - return new ReleaseNotes(version, startingRef, endingRef); - }); - } - /** Retrieve the release note generated for a Github Release. */ - getGithubReleaseEntry() { - return tslib.__awaiter(this, void 0, void 0, function* () { - return ejs.render(githubReleaseTemplate, yield this.generateRenderContext(), { rmWhitespace: true }); - }); - } - /** Retrieve the release note generated for a CHANGELOG entry. */ - getChangelogEntry() { - return tslib.__awaiter(this, void 0, void 0, function* () { - return ejs.render(changelogTemplate, yield this.generateRenderContext(), { rmWhitespace: true }); - }); - } - /** - * Prompt the user for a title for the release, if the project's configuration is defined to use a - * title. - */ - promptForReleaseTitle() { - return tslib.__awaiter(this, void 0, void 0, function* () { - if (this.title === undefined) { - if (this.config.useReleaseTitle) { - this.title = yield promptInput('Please provide a title for the release:'); - } - else { - this.title = false; - } - } - return this.title; - }); - } - /** Build the render context data object for constructing the RenderContext instance. */ - generateRenderContext() { - return tslib.__awaiter(this, void 0, void 0, function* () { - if (!this.renderContext) { - this.renderContext = new RenderContext({ - commits: yield this.commits, - github: this.git.remoteConfig, - version: this.version.format(), - groupOrder: this.config.groupOrder, - hiddenScopes: this.config.hiddenScopes, - title: yield this.promptForReleaseTitle(), - }); - } - return this.renderContext; - }); - } - // These methods are used for access to the utility functions while allowing them to be - // overwritten in subclasses during testing. - getCommitsInRange(from, to) { - return tslib.__awaiter(this, void 0, void 0, function* () { - return getCommitsInRange(from, to); - }); - } - getReleaseConfig(config) { - return getReleaseConfig(config); - } -} - -/** - * @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 - */ -/** Yargs command builder for configuring the `ng-dev release build` command. */ -function builder$9(argv) { - return argv - .option('releaseVersion', { type: 'string', default: '0.0.0', coerce: (version) => new semver.SemVer(version) }) - .option('from', { - type: 'string', - description: 'The git tag or ref to start the changelog entry from', - defaultDescription: 'The latest semver tag', - }) - .option('to', { - type: 'string', - description: 'The git tag or ref to end the changelog entry with', - default: 'HEAD', - }) - .option('type', { - type: 'string', - description: 'The type of release notes to create', - choices: ['github-release', 'changelog'], - default: 'changelog', - }) - .option('outFile', { - type: 'string', - description: 'File location to write the generated release notes to', - coerce: (filePath) => filePath ? path.join(process.cwd(), filePath) : undefined - }); -} -/** Yargs command handler for generating release notes. */ -function handler$a({ releaseVersion, from, to, outFile, type }) { - return tslib.__awaiter(this, void 0, void 0, function* () { - // Since `yargs` evaluates defaults even if a value as been provided, if no value is provided to - // the handler, the latest semver tag on the branch is used. - from = from || GitClient.get().getLatestSemverTag().format(); - /** The ReleaseNotes instance to generate release notes. */ - const releaseNotes = yield ReleaseNotes.fromRange(releaseVersion, from, to); - /** The requested release notes entry. */ - const releaseNotesEntry = yield (type === 'changelog' ? releaseNotes.getChangelogEntry() : - releaseNotes.getGithubReleaseEntry()); - if (outFile) { - fs.writeFileSync(outFile, releaseNotesEntry); - info(`Generated release notes for "${releaseVersion}" written to ${outFile}`); - } - else { - process.stdout.write(releaseNotesEntry); - } - }); -} -/** CLI command module for generating release notes. */ -const ReleaseNotesCommandModule = { - builder: builder$9, - handler: handler$a, - command: 'notes', - describe: 'Generate release notes', -}; - -/** - * @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 - */ -/** - * Runs NPM publish within a specified package directory. - * @throws With the process log output if the publish failed. - */ -function runNpmPublish(packagePath, distTag, registryUrl) { - return tslib.__awaiter(this, void 0, void 0, function* () { - const args = ['publish', '--access', 'public', '--tag', distTag]; - // If a custom registry URL has been specified, add the `--registry` flag. - if (registryUrl !== undefined) { - args.push('--registry', registryUrl); - } - yield spawn('npm', args, { cwd: packagePath, mode: 'silent' }); - }); -} -/** - * Sets the NPM tag to the specified version for the given package. - * @throws With the process log output if the tagging failed. - */ -function setNpmTagForPackage(packageName, distTag, version, registryUrl) { - return tslib.__awaiter(this, void 0, void 0, function* () { - const args = ['dist-tag', 'add', `${packageName}@${version}`, distTag]; - // If a custom registry URL has been specified, add the `--registry` flag. - if (registryUrl !== undefined) { - args.push('--registry', registryUrl); - } - yield spawn('npm', args, { mode: 'silent' }); - }); -} -/** - * Checks whether the user is currently logged into NPM. - * @returns Whether the user is currently logged into NPM. - */ -function npmIsLoggedIn(registryUrl) { - return tslib.__awaiter(this, void 0, void 0, function* () { - const args = ['whoami']; - // If a custom registry URL has been specified, add the `--registry` flag. - if (registryUrl !== undefined) { - args.push('--registry', registryUrl); - } - try { - yield spawn('npm', args, { mode: 'silent' }); - } - catch (e) { - return false; - } - return true; - }); -} -/** - * Log into NPM at a provided registry. - * @throws With the `npm login` status code if the login failed. - */ -function npmLogin(registryUrl) { - return tslib.__awaiter(this, void 0, void 0, function* () { - const args = ['login', '--no-browser']; - // If a custom registry URL has been specified, add the `--registry` flag. The `--registry` flag - // must be spliced into the correct place in the command as npm expects it to be the flag - // immediately following the login subcommand. - if (registryUrl !== undefined) { - args.splice(1, 0, '--registry', registryUrl); - } - // The login command prompts for username, password and other profile information. Hence - // the process needs to be interactive (i.e. respecting current TTYs stdin). - yield spawnInteractive('npm', args); - }); -} -/** - * Log out of NPM at a provided registry. - * @returns Whether the user was logged out of NPM. - */ -function npmLogout(registryUrl) { - return tslib.__awaiter(this, void 0, void 0, function* () { - const args = ['logout']; - // If a custom registry URL has been specified, add the `--registry` flag. The `--registry` flag - // must be spliced into the correct place in the command as npm expects it to be the flag - // immediately following the logout subcommand. - if (registryUrl !== undefined) { - args.splice(1, 0, '--registry', registryUrl); - } - try { - yield spawn('npm', args, { mode: 'silent' }); - } - finally { - return npmIsLoggedIn(registryUrl); - } - }); -} - -/** - * @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 that will be thrown if the user manually aborted a release action. */ -class UserAbortedReleaseActionError extends Error { - constructor() { - super(); - // 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, UserAbortedReleaseActionError.prototype); - } -} -/** Error that will be thrown if the action has been aborted due to a fatal error. */ -class FatalReleaseActionError extends Error { - constructor() { - super(); - // 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, FatalReleaseActionError.prototype); - } -} - -/** - * @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 - */ -/** - * Increments a specified SemVer version. Compared to the original increment in SemVer, - * the version is cloned to not modify the original version instance. - */ -function semverInc(version, release, identifier) { - var clone = new semver.SemVer(version.version); - return clone.inc(release, identifier); -} -/** Creates the equivalent experimental version for a provided SemVer. */ -function createExperimentalSemver(version) { - version = new semver.SemVer(version); - var experimentalVersion = new semver.SemVer(version.format()); - experimentalVersion.major = 0; - experimentalVersion.minor = version.major * 100 + version.minor; - return new semver.SemVer(experimentalVersion.format()); -} - -/** - * @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 - */ -/** Gets the commit message for a new release point in the project. */ -function getCommitMessageForRelease(newVersion) { - return `release: cut the v${newVersion} release`; -} -/** - * Gets the commit message for an exceptional version bump in the next branch. The next - * branch version will be bumped without the release being published in some situations. - * More details can be found in the `MoveNextIntoFeatureFreeze` release action and in: - * https://hackmd.io/2Le8leq0S6G_R5VEVTNK9A. - */ -function getCommitMessageForExceptionalNextVersionBump(newVersion) { - return `release: bump the next branch to v${newVersion}`; -} -/** Gets the commit message for a release notes cherry-pick commit */ -function getReleaseNoteCherryPickCommitMessage(newVersion) { - return `docs: release notes for the v${newVersion} 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 - */ -/** Project-relative path for the "package.json" file. */ -const packageJsonPath = 'package.json'; -/** Project-relative path for the changelog file. */ -const changelogPath = 'CHANGELOG.md'; -/** Default interval in milliseconds to check whether a pull request has been merged. */ -const waitForPullRequestInterval = 10000; - -/** - * @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 - */ -/* - * ############################################################### - * - * This file contains helpers for invoking external `ng-dev` commands. A subset of actions, - * like building release output or setting aν NPM dist tag for release packages, cannot be - * performed directly as part of the release tool and need to be delegated to external `ng-dev` - * commands that exist across arbitrary version branches. - * - * In a concrete example: Consider a new patch version is released and that a new release - * package has been added to the `next` branch. The patch branch will not contain the new - * release package, so we could not build the release output for it. To work around this, we - * call the ng-dev build command for the patch version branch and expect it to return a list - * of built packages that need to be released as part of this release train. - * - * ############################################################### - */ -/** - * Invokes the `ng-dev release set-dist-tag` command in order to set the specified - * NPM dist tag for all packages in the checked out branch to the given version. - */ -function invokeSetNpmDistCommand(npmDistTag, version) { - return tslib.__awaiter(this, void 0, void 0, function* () { - try { - // Note: No progress indicator needed as that is the responsibility of the command. - yield spawn('yarn', ['--silent', 'ng-dev', 'release', 'set-dist-tag', npmDistTag, version.format()]); - info(green(` ✓ Set "${npmDistTag}" NPM dist tag for all packages to v${version}.`)); - } - catch (e) { - error(e); - error(red(` ✘ An error occurred while setting the NPM dist tag for "${npmDistTag}".`)); - throw new FatalReleaseActionError(); - } - }); -} -/** - * Invokes the `ng-dev release build` command in order to build the release - * packages for the currently checked out branch. - */ -function invokeReleaseBuildCommand() { - return tslib.__awaiter(this, void 0, void 0, function* () { - const spinner = ora.call(undefined).start('Building release output.'); - try { - // Since we expect JSON to be printed from the `ng-dev release build` command, - // we spawn the process in silent mode. We have set up an Ora progress spinner. - const { stdout } = yield spawn('yarn', ['--silent', 'ng-dev', 'release', 'build', '--json'], { mode: 'silent' }); - spinner.stop(); - info(green(' ✓ Built release output for all packages.')); - // The `ng-dev release build` command prints a JSON array to stdout - // that represents the built release packages and their output paths. - return JSON.parse(stdout.trim()); - } - catch (e) { - spinner.stop(); - error(e); - error(red(' ✘ An error occurred while building the release packages.')); - throw new FatalReleaseActionError(); - } - }); -} -/** - * Invokes the `yarn install` command in order to install dependencies for - * the configured project with the currently checked out revision. - */ -function invokeYarnInstallCommand(projectDir) { - return tslib.__awaiter(this, void 0, void 0, function* () { - try { - // Note: No progress indicator needed as that is the responsibility of the command. - // TODO: Consider using an Ora spinner instead to ensure minimal console output. - yield spawn('yarn', ['install', '--frozen-lockfile', '--non-interactive'], { cwd: projectDir }); - info(green(' ✓ Installed project dependencies.')); - } - catch (e) { - error(e); - error(red(' ✘ An error occurred while installing dependencies.')); - throw new FatalReleaseActionError(); - } - }); -} - -/** - * @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 - */ -/** - * Graphql Github API query that can be used to find forks of a given repository - * that are owned by the current viewer authenticated with the Github API. - */ -const findOwnedForksOfRepoQuery = typedGraphqlify.params({ - $owner: 'String!', - $name: 'String!', -}, { - repository: typedGraphqlify.params({ owner: '$owner', name: '$name' }, { - forks: typedGraphqlify.params({ affiliations: 'OWNER', first: 1 }, { - nodes: [{ - owner: { - login: typedGraphqlify.types.string, - }, - name: typedGraphqlify.types.string, - }], - }), - }), -}); - -/** - * @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 - */ -/** Thirty seconds in milliseconds. */ -const THIRTY_SECONDS_IN_MS = 30000; -/** Gets whether a given pull request has been merged. */ -function getPullRequestState(api, id) { - return tslib.__awaiter(this, void 0, void 0, function* () { - const { data } = yield api.github.pulls.get(Object.assign(Object.assign({}, api.remoteParams), { pull_number: id })); - if (data.merged) { - return 'merged'; - } - // Check if the PR was closed more than 30 seconds ago, this extra time gives Github time to - // update the closed pull request to be associated with the closing commit. - // Note: a Date constructed with `null` creates an object at 0 time, which will never be greater - // than the current date time. - if (data.closed_at !== null && - (new Date(data.closed_at).getTime() < Date.now() - THIRTY_SECONDS_IN_MS)) { - return (yield isPullRequestClosedWithAssociatedCommit(api, id)) ? 'merged' : 'closed'; - } - return 'open'; - }); -} -/** - * Whether the pull request has been closed with an associated commit. This is usually - * the case if a PR has been merged using the autosquash merge script strategy. Since - * the merge is not fast-forward, Github does not consider the PR as merged and instead - * shows the PR as closed. See for example: https://github.com/angular/angular/pull/37918. - */ -function isPullRequestClosedWithAssociatedCommit(api, id) { - return tslib.__awaiter(this, void 0, void 0, function* () { - const events = yield api.github.paginate(api.github.issues.listEvents, Object.assign(Object.assign({}, api.remoteParams), { issue_number: id })); - // Iterate through the events of the pull request in reverse. We want to find the most - // recent events and check if the PR has been closed with a commit associated with it. - // If the PR has been closed through a commit, we assume that the PR has been merged - // using the autosquash merge strategy. For more details. See the `AutosquashMergeStrategy`. - for (let i = events.length - 1; i >= 0; i--) { - const { event, commit_id } = events[i]; - // If we come across a "reopened" event, we abort looking for referenced commits. Any - // commits that closed the PR before, are no longer relevant and did not close the PR. - if (event === 'reopened') { - return false; - } - // If a `closed` event is captured with a commit assigned, then we assume that - // this PR has been merged properly. - if (event === 'closed' && commit_id) { - return true; - } - // If the PR has been referenced by a commit, check if the commit closes this pull - // request. Note that this is needed besides checking `closed` as PRs could be merged - // into any non-default branch where the `Closes <..>` keyword does not work and the PR - // is simply closed without an associated `commit_id`. For more details see: - // https://docs.github.com/en/enterprise/2.16/user/github/managing-your-work-on-github/closing-issues-using-keywords#:~:text=non-default. - if (event === 'referenced' && commit_id && - (yield isCommitClosingPullRequest(api, commit_id, id))) { - return true; - } - } - return false; - }); -} -/** Checks whether the specified commit is closing the given pull request. */ -function isCommitClosingPullRequest(api, sha, id) { - return tslib.__awaiter(this, void 0, void 0, function* () { - const { data } = yield api.github.repos.getCommit(Object.assign(Object.assign({}, api.remoteParams), { ref: sha })); - // Matches the closing keyword supported in commit messages. See: - // https://docs.github.com/en/enterprise/2.16/user/github/managing-your-work-on-github/closing-issues-using-keywords. - return data.commit.message.match(new RegExp(`(?:close[sd]?|fix(?:e[sd]?)|resolve[sd]?):? #${id}(?!\\d)`, 'i')); - }); -} - -/** - * @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 - */ -/** - * Abstract base class for a release action. A release action is selectable by the caretaker - * if active, and can perform changes for releasing, such as staging a release, bumping the - * version, cherry-picking the changelog, branching off from master. etc. - */ -class ReleaseAction { - constructor(active, git, config, projectDir) { - this.active = active; - this.git = git; - this.config = config; - this.projectDir = projectDir; - /** Cached found fork of the configured project. */ - this._cachedForkRepo = null; - } - /** Whether the release action is currently active. */ - static isActive(_trains, _config) { - throw Error('Not implemented.'); - } - /** Retrieves the version in the project top-level `package.json` file. */ - getProjectVersion() { - return tslib.__awaiter(this, void 0, void 0, function* () { - const pkgJsonPath = path.join(this.projectDir, packageJsonPath); - const pkgJson = JSON.parse(yield fs.promises.readFile(pkgJsonPath, 'utf8')); - return new semver.SemVer(pkgJson.version); - }); - } - /** Updates the version in the project top-level `package.json` file. */ - updateProjectVersion(newVersion) { - return tslib.__awaiter(this, void 0, void 0, function* () { - const pkgJsonPath = path.join(this.projectDir, packageJsonPath); - const pkgJson = JSON.parse(yield fs.promises.readFile(pkgJsonPath, 'utf8')); - pkgJson.version = newVersion.format(); - // Write the `package.json` file. Note that we add a trailing new line - // to avoid unnecessary diff. IDEs usually add a trailing new line. - yield fs.promises.writeFile(pkgJsonPath, `${JSON.stringify(pkgJson, null, 2)}\n`); - info(green(` ✓ Updated project version to ${pkgJson.version}`)); - }); - } - /** Gets the most recent commit of a specified branch. */ - _getCommitOfBranch(branchName) { - return tslib.__awaiter(this, void 0, void 0, function* () { - const { data: { commit } } = yield this.git.github.repos.getBranch(Object.assign(Object.assign({}, this.git.remoteParams), { branch: branchName })); - return commit.sha; - }); - } - /** Verifies that the latest commit for the given branch is passing all statuses. */ - verifyPassingGithubStatus(branchName) { - return tslib.__awaiter(this, void 0, void 0, function* () { - const commitSha = yield this._getCommitOfBranch(branchName); - const { data: { state } } = yield this.git.github.repos.getCombinedStatusForRef(Object.assign(Object.assign({}, this.git.remoteParams), { ref: commitSha })); - const branchCommitsUrl = getListCommitsInBranchUrl(this.git, branchName); - if (state === 'failure') { - error(red(` ✘ Cannot stage release. Commit "${commitSha}" does not pass all github ` + - 'status checks. Please make sure this commit passes all checks before re-running.')); - error(` Please have a look at: ${branchCommitsUrl}`); - if (yield promptConfirm('Do you want to ignore the Github status and proceed?')) { - info(yellow(' ⚠ Upstream commit is failing CI checks, but status has been forcibly ignored.')); - return; - } - throw new UserAbortedReleaseActionError(); - } - else if (state === 'pending') { - error(red(` ✘ Commit "${commitSha}" still has pending github statuses that ` + - 'need to succeed before staging a release.')); - error(red(` Please have a look at: ${branchCommitsUrl}`)); - if (yield promptConfirm('Do you want to ignore the Github status and proceed?')) { - info(yellow(' ⚠ Upstream commit is pending CI, but status has been forcibly ignored.')); - return; - } - throw new UserAbortedReleaseActionError(); - } - info(green(' ✓ Upstream commit is passing all github status checks.')); - }); - } - /** - * Prompts the user for potential release notes edits that need to be made. Once - * confirmed, a new commit for the release point is created. - */ - waitForEditsAndCreateReleaseCommit(newVersion) { - return tslib.__awaiter(this, void 0, void 0, function* () { - info(yellow(' ⚠ Please review the changelog and ensure that the log contains only changes ' + - 'that apply to the public API surface. Manual changes can be made. When done, please ' + - 'proceed with the prompt below.')); - if (!(yield promptConfirm('Do you want to proceed and commit the changes?'))) { - throw new UserAbortedReleaseActionError(); - } - // Commit message for the release point. - const commitMessage = getCommitMessageForRelease(newVersion); - // Create a release staging commit including changelog and version bump. - yield this.createCommit(commitMessage, [packageJsonPath, changelogPath]); - info(green(` ✓ Created release commit for: "${newVersion}".`)); - }); - } - /** - * Gets an owned fork for the configured project of the authenticated user. Aborts the - * process with an error if no fork could be found. Also caches the determined fork - * repository as the authenticated user cannot change during action execution. - */ - _getForkOfAuthenticatedUser() { - return tslib.__awaiter(this, void 0, void 0, function* () { - if (this._cachedForkRepo !== null) { - return this._cachedForkRepo; - } - const { owner, name } = this.git.remoteConfig; - const result = yield this.git.github.graphql(findOwnedForksOfRepoQuery, { owner, name }); - const forks = result.repository.forks.nodes; - if (forks.length === 0) { - error(red(' ✘ Unable to find fork for currently authenticated user.')); - error(red(` Please ensure you created a fork of: ${owner}/${name}.`)); - throw new FatalReleaseActionError(); - } - const fork = forks[0]; - return this._cachedForkRepo = { owner: fork.owner.login, name: fork.name }; - }); - } - /** Checks whether a given branch name is reserved in the specified repository. */ - _isBranchNameReservedInRepo(repo, name) { - return tslib.__awaiter(this, void 0, void 0, function* () { - try { - yield this.git.github.repos.getBranch({ owner: repo.owner, repo: repo.name, branch: name }); - return true; - } - catch (e) { - // If the error has a `status` property set to `404`, then we know that the branch - // does not exist. Otherwise, it might be an API error that we want to report/re-throw. - if (e.status === 404) { - return false; - } - throw e; - } - }); - } - /** Finds a non-reserved branch name in the repository with respect to a base name. */ - _findAvailableBranchName(repo, baseName) { - return tslib.__awaiter(this, void 0, void 0, function* () { - let currentName = baseName; - let suffixNum = 0; - while (yield this._isBranchNameReservedInRepo(repo, currentName)) { - suffixNum++; - currentName = `${baseName}_${suffixNum}`; - } - return currentName; - }); - } - /** - * Creates a local branch from the current Git `HEAD`. Will override - * existing branches in case of a collision. - */ - createLocalBranchFromHead(branchName) { - return tslib.__awaiter(this, void 0, void 0, function* () { - this.git.run(['checkout', '-q', '-B', branchName]); - }); - } - /** Pushes the current Git `HEAD` to the given remote branch in the configured project. */ - pushHeadToRemoteBranch(branchName) { - return tslib.__awaiter(this, void 0, void 0, function* () { - // Push the local `HEAD` to the remote branch in the configured project. - this.git.run(['push', '-q', this.git.getRepoGitUrl(), `HEAD:refs/heads/${branchName}`]); - }); - } - /** - * Pushes the current Git `HEAD` to a fork for the configured project that is owned by - * the authenticated user. If the specified branch name exists in the fork already, a - * unique one will be generated based on the proposed name to avoid collisions. - * @param proposedBranchName Proposed branch name for the fork. - * @param trackLocalBranch Whether the fork branch should be tracked locally. i.e. whether - * a local branch with remote tracking should be set up. - * @returns The fork and branch name containing the pushed changes. - */ - _pushHeadToFork(proposedBranchName, trackLocalBranch) { - return tslib.__awaiter(this, void 0, void 0, function* () { - const fork = yield this._getForkOfAuthenticatedUser(); - // Compute a repository URL for pushing to the fork. Note that we want to respect - // the SSH option from the dev-infra github configuration. - const repoGitUrl = getRepositoryGitUrl(Object.assign(Object.assign({}, fork), { useSsh: this.git.remoteConfig.useSsh }), this.git.githubToken); - const branchName = yield this._findAvailableBranchName(fork, proposedBranchName); - const pushArgs = []; - // If a local branch should track the remote fork branch, create a branch matching - // the remote branch. Later with the `git push`, the remote is set for the branch. - if (trackLocalBranch) { - yield this.createLocalBranchFromHead(branchName); - pushArgs.push('--set-upstream'); - } - // Push the local `HEAD` to the remote branch in the fork. - this.git.run(['push', '-q', repoGitUrl, `HEAD:refs/heads/${branchName}`, ...pushArgs]); - return { fork, branchName }; - }); - } - /** - * Pushes changes to a fork for the configured project that is owned by the currently - * authenticated user. A pull request is then created for the pushed changes on the - * configured project that targets the specified target branch. - * @returns An object describing the created pull request. - */ - pushChangesToForkAndCreatePullRequest(targetBranch, proposedForkBranchName, title, body) { - return tslib.__awaiter(this, void 0, void 0, function* () { - const repoSlug = `${this.git.remoteParams.owner}/${this.git.remoteParams.repo}`; - const { fork, branchName } = yield this._pushHeadToFork(proposedForkBranchName, true); - const { data } = yield this.git.github.pulls.create(Object.assign(Object.assign({}, this.git.remoteParams), { head: `${fork.owner}:${branchName}`, base: targetBranch, body, - title })); - // Add labels to the newly created PR if provided in the configuration. - if (this.config.releasePrLabels !== undefined) { - yield this.git.github.issues.addLabels(Object.assign(Object.assign({}, this.git.remoteParams), { issue_number: data.number, labels: this.config.releasePrLabels })); - } - info(green(` ✓ Created pull request #${data.number} in ${repoSlug}.`)); - return { - id: data.number, - url: data.html_url, - fork, - forkBranch: branchName, - }; - }); - } - /** - * Waits for the given pull request to be merged. Default interval for checking the Github - * API is 10 seconds (to not exceed any rate limits). If the pull request is closed without - * merge, the script will abort gracefully (considering a manual user abort). - */ - waitForPullRequestToBeMerged({ id }, interval = waitForPullRequestInterval) { - return tslib.__awaiter(this, void 0, void 0, function* () { - return new Promise((resolve, reject) => { - debug(`Waiting for pull request #${id} to be merged.`); - const spinner = ora.call(undefined).start(`Waiting for pull request #${id} to be merged.`); - const intervalId = setInterval(() => tslib.__awaiter(this, void 0, void 0, function* () { - const prState = yield getPullRequestState(this.git, id); - if (prState === 'merged') { - spinner.stop(); - info(green(` ✓ Pull request #${id} has been merged.`)); - clearInterval(intervalId); - resolve(); - } - else if (prState === 'closed') { - spinner.stop(); - warn(yellow(` ✘ Pull request #${id} has been closed.`)); - clearInterval(intervalId); - reject(new UserAbortedReleaseActionError()); - } - }), interval); - }); - }); - } - /** - * Prepend releases notes for a version published in a given branch to the changelog in - * the current Git `HEAD`. This is useful for cherry-picking the changelog. - * @returns A boolean indicating whether the release notes have been prepended. - */ - prependReleaseNotesToChangelog(releaseNotes) { - return tslib.__awaiter(this, void 0, void 0, function* () { - const localChangelogPath = path.join(this.projectDir, changelogPath); - const localChangelog = yield fs.promises.readFile(localChangelogPath, 'utf8'); - const releaseNotesEntry = yield releaseNotes.getChangelogEntry(); - yield fs.promises.writeFile(localChangelogPath, `${releaseNotesEntry}\n\n${localChangelog}`); - info(green(` ✓ Updated the changelog to capture changes for "${releaseNotes.version}".`)); - }); - } - /** Checks out an upstream branch with a detached head. */ - checkoutUpstreamBranch(branchName) { - return tslib.__awaiter(this, void 0, void 0, function* () { - this.git.run(['fetch', '-q', this.git.getRepoGitUrl(), branchName]); - this.git.run(['checkout', '-q', 'FETCH_HEAD', '--detach']); - }); - } - /** - * Creates a commit for the specified files with the given message. - * @param message Message for the created commit - * @param files List of project-relative file paths to be commited. - */ - createCommit(message, files) { - return tslib.__awaiter(this, void 0, void 0, function* () { - this.git.run(['commit', '-q', '--no-verify', '-m', message, ...files]); - }); - } - /** - * Stages the specified new version for the current branch and creates a - * pull request that targets the given base branch. - * @returns an object describing the created pull request. - */ - stageVersionForBranchAndCreatePullRequest(newVersion, pullRequestBaseBranch) { - return tslib.__awaiter(this, void 0, void 0, function* () { - /** - * The current version of the project for the branch from the root package.json. This must be - * retrieved prior to updating the project version. - */ - const currentVersion = this.git.getMatchingTagForSemver(yield this.getProjectVersion()); - const releaseNotes = yield ReleaseNotes.fromRange(newVersion, currentVersion, 'HEAD'); - yield this.updateProjectVersion(newVersion); - yield this.prependReleaseNotesToChangelog(releaseNotes); - yield this.waitForEditsAndCreateReleaseCommit(newVersion); - const pullRequest = yield this.pushChangesToForkAndCreatePullRequest(pullRequestBaseBranch, `release-stage-${newVersion}`, `Bump version to "v${newVersion}" with changelog.`); - info(green(' ✓ Release staging pull request has been created.')); - info(yellow(` Please ask team members to review: ${pullRequest.url}.`)); - return { releaseNotes, pullRequest }; - }); - } - /** - * Checks out the specified target branch, verifies its CI status and stages - * the specified new version in order to create a pull request. - * @returns an object describing the created pull request. - */ - checkoutBranchAndStageVersion(newVersion, stagingBranch) { - return tslib.__awaiter(this, void 0, void 0, function* () { - yield this.verifyPassingGithubStatus(stagingBranch); - yield this.checkoutUpstreamBranch(stagingBranch); - return yield this.stageVersionForBranchAndCreatePullRequest(newVersion, stagingBranch); - }); - } - /** - * Cherry-picks the release notes of a version that have been pushed to a given branch - * into the `next` primary development branch. A pull request is created for this. - * @returns a boolean indicating successful creation of the cherry-pick pull request. - */ - cherryPickChangelogIntoNextBranch(releaseNotes, stagingBranch) { - return tslib.__awaiter(this, void 0, void 0, function* () { - const nextBranch = this.active.next.branchName; - const commitMessage = getReleaseNoteCherryPickCommitMessage(releaseNotes.version); - // Checkout the next branch. - yield this.checkoutUpstreamBranch(nextBranch); - yield this.prependReleaseNotesToChangelog(releaseNotes); - // Create a changelog cherry-pick commit. - yield this.createCommit(commitMessage, [changelogPath]); - info(green(` ✓ Created changelog cherry-pick commit for: "${releaseNotes.version}".`)); - // Create a cherry-pick pull request that should be merged by the caretaker. - const pullRequest = yield this.pushChangesToForkAndCreatePullRequest(nextBranch, `changelog-cherry-pick-${releaseNotes.version}`, commitMessage, `Cherry-picks the changelog from the "${stagingBranch}" branch to the next ` + - `branch (${nextBranch}).`); - info(green(` ✓ Pull request for cherry-picking the changelog into "${nextBranch}" ` + - 'has been created.')); - info(yellow(` Please ask team members to review: ${pullRequest.url}.`)); - // Wait for the Pull Request to be merged. - yield this.waitForPullRequestToBeMerged(pullRequest); - return true; - }); - } - /** - * Creates a Github release for the specified version in the configured project. - * The release is created by tagging the specified commit SHA. - */ - _createGithubReleaseForVersion(releaseNotes, versionBumpCommitSha, prerelease) { - return tslib.__awaiter(this, void 0, void 0, function* () { - const tagName = releaseNotes.version.format(); - yield this.git.github.git.createRef(Object.assign(Object.assign({}, this.git.remoteParams), { ref: `refs/tags/${tagName}`, sha: versionBumpCommitSha })); - info(green(` ✓ Tagged v${releaseNotes.version} release upstream.`)); - yield this.git.github.repos.createRelease(Object.assign(Object.assign({}, this.git.remoteParams), { name: `v${releaseNotes.version}`, tag_name: tagName, prerelease, body: yield releaseNotes.getGithubReleaseEntry() })); - info(green(` ✓ Created v${releaseNotes.version} release in Github.`)); - }); - } - /** - * Builds and publishes the given version in the specified branch. - * @param releaseNotes The release notes for the version being published. - * @param publishBranch Name of the branch that contains the new version. - * @param npmDistTag NPM dist tag where the version should be published to. - */ - buildAndPublish(releaseNotes, publishBranch, npmDistTag) { - return tslib.__awaiter(this, void 0, void 0, function* () { - const versionBumpCommitSha = yield this._getCommitOfBranch(publishBranch); - if (!(yield this._isCommitForVersionStaging(releaseNotes.version, versionBumpCommitSha))) { - error(red(` ✘ Latest commit in "${publishBranch}" branch is not a staging commit.`)); - error(red(' Please make sure the staging pull request has been merged.')); - throw new FatalReleaseActionError(); - } - // Checkout the publish branch and build the release packages. - yield this.checkoutUpstreamBranch(publishBranch); - // Install the project dependencies for the publish branch, and then build the release - // packages. Note that we do not directly call the build packages function from the release - // config. We only want to build and publish packages that have been configured in the given - // publish branch. e.g. consider we publish patch version and a new package has been - // created in the `next` branch. The new package would not be part of the patch branch, - // so we cannot build and publish it. - yield invokeYarnInstallCommand(this.projectDir); - const builtPackages = yield invokeReleaseBuildCommand(); - // Verify the packages built are the correct version. - yield this._verifyPackageVersions(releaseNotes.version, builtPackages); - // Create a Github release for the new version. - yield this._createGithubReleaseForVersion(releaseNotes, versionBumpCommitSha, npmDistTag === 'next'); - // Walk through all built packages and publish them to NPM. - for (const builtPackage of builtPackages) { - yield this._publishBuiltPackageToNpm(builtPackage, npmDistTag); - } - info(green(' ✓ Published all packages successfully')); - }); - } - /** Publishes the given built package to NPM with the specified NPM dist tag. */ - _publishBuiltPackageToNpm(pkg, npmDistTag) { - return tslib.__awaiter(this, void 0, void 0, function* () { - debug(`Starting publish of "${pkg.name}".`); - const spinner = ora.call(undefined).start(`Publishing "${pkg.name}"`); - try { - yield runNpmPublish(pkg.outputPath, npmDistTag, this.config.publishRegistry); - spinner.stop(); - info(green(` ✓ Successfully published "${pkg.name}.`)); - } - catch (e) { - spinner.stop(); - error(e); - error(red(` ✘ An error occurred while publishing "${pkg.name}".`)); - throw new FatalReleaseActionError(); - } - }); - } - /** Checks whether the given commit represents a staging commit for the specified version. */ - _isCommitForVersionStaging(version, commitSha) { - return tslib.__awaiter(this, void 0, void 0, function* () { - const { data } = yield this.git.github.repos.getCommit(Object.assign(Object.assign({}, this.git.remoteParams), { ref: commitSha })); - return data.commit.message.startsWith(getCommitMessageForRelease(version)); - }); - } - /** Verify the version of each generated package exact matches the specified version. */ - _verifyPackageVersions(version, packages) { - return tslib.__awaiter(this, void 0, void 0, function* () { - /** Experimental equivalent version for packages created with the provided version. */ - const experimentalVersion = createExperimentalSemver(version); - for (const pkg of packages) { - const { version: packageJsonVersion } = JSON.parse(yield fs.promises.readFile(path.join(pkg.outputPath, 'package.json'), 'utf8')); - const mismatchesVersion = version.compare(packageJsonVersion) !== 0; - const mismatchesExperimental = experimentalVersion.compare(packageJsonVersion) !== 0; - if (mismatchesExperimental && mismatchesVersion) { - error(red('The built package version does not match the version being released.')); - error(` Release Version: ${version.version} (${experimentalVersion.version})`); - error(` Generated Version: ${packageJsonVersion}`); - throw new FatalReleaseActionError(); - } - } - }); - } -} - -/** - * @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 - */ -/** - * Release action that cuts a new patch release for an active release-train in the long-term - * support phase. The patch segment is incremented. The changelog is generated for the new - * patch version, but also needs to be cherry-picked into the next development branch. - */ -class CutLongTermSupportPatchAction extends ReleaseAction { - constructor() { - super(...arguments); - /** Promise resolving an object describing long-term support branches. */ - this.ltsBranches = fetchLongTermSupportBranchesFromNpm(this.config); - } - getDescription() { - return tslib.__awaiter(this, void 0, void 0, function* () { - const { active } = yield this.ltsBranches; - return `Cut a new release for an active LTS branch (${active.length} active).`; - }); - } - perform() { - return tslib.__awaiter(this, void 0, void 0, function* () { - const ltsBranch = yield this._promptForTargetLtsBranch(); - const newVersion = semverInc(ltsBranch.version, 'patch'); - const { pullRequest, releaseNotes } = yield this.checkoutBranchAndStageVersion(newVersion, ltsBranch.name); - yield this.waitForPullRequestToBeMerged(pullRequest); - yield this.buildAndPublish(releaseNotes, ltsBranch.name, ltsBranch.npmDistTag); - yield this.cherryPickChangelogIntoNextBranch(releaseNotes, ltsBranch.name); - }); - } - /** Prompts the user to select an LTS branch for which a patch should but cut. */ - _promptForTargetLtsBranch() { - return tslib.__awaiter(this, void 0, void 0, function* () { - const { active, inactive } = yield this.ltsBranches; - const activeBranchChoices = active.map(branch => this._getChoiceForLtsBranch(branch)); - // If there are inactive LTS branches, we allow them to be selected. In some situations, - // patch releases are still cut for inactive LTS branches. e.g. when the LTS duration - // has been increased due to exceptional events () - if (inactive.length !== 0) { - activeBranchChoices.push({ name: 'Inactive LTS versions (not recommended)', value: null }); - } - const { activeLtsBranch, inactiveLtsBranch } = yield inquirer.prompt([ - { - name: 'activeLtsBranch', - type: 'list', - message: 'Please select a version for which you want to cut an LTS patch', - choices: activeBranchChoices, - }, - { - name: 'inactiveLtsBranch', - type: 'list', - when: o => o.activeLtsBranch === null, - message: 'Please select an inactive LTS version for which you want to cut an LTS patch', - choices: inactive.map(branch => this._getChoiceForLtsBranch(branch)), - } - ]); - return activeLtsBranch !== null && activeLtsBranch !== void 0 ? activeLtsBranch : inactiveLtsBranch; - }); - } - /** Gets an inquirer choice for the given LTS branch. */ - _getChoiceForLtsBranch(branch) { - return { name: `v${branch.version.major} (from ${branch.name})`, value: branch }; - } - static isActive(active) { - return tslib.__awaiter(this, void 0, void 0, function* () { - // LTS patch versions can be only cut if there are release trains in LTS phase. - // This action is always selectable as we support publishing of old LTS branches, - // and have prompt for selecting an LTS branch when the action performs. - return true; - }); - } -} - -/** - * @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 - */ -/** - * Release action that cuts a new patch release for the current latest release-train version - * branch (i.e. the patch branch). The patch segment is incremented. The changelog is generated - * for the new patch version, but also needs to be cherry-picked into the next development branch. - */ -class CutNewPatchAction extends ReleaseAction { - constructor() { - super(...arguments); - this._newVersion = semverInc(this.active.latest.version, 'patch'); - } - getDescription() { - return tslib.__awaiter(this, void 0, void 0, function* () { - const { branchName } = this.active.latest; - const newVersion = this._newVersion; - return `Cut a new patch release for the "${branchName}" branch (v${newVersion}).`; - }); - } - perform() { - return tslib.__awaiter(this, void 0, void 0, function* () { - const { branchName } = this.active.latest; - const newVersion = this._newVersion; - const { pullRequest, releaseNotes } = yield this.checkoutBranchAndStageVersion(newVersion, branchName); - yield this.waitForPullRequestToBeMerged(pullRequest); - yield this.buildAndPublish(releaseNotes, branchName, 'latest'); - yield this.cherryPickChangelogIntoNextBranch(releaseNotes, branchName); - }); - } - static isActive(active) { - return tslib.__awaiter(this, void 0, void 0, function* () { - // Patch versions can be cut at any time. See: - // https://hackmd.io/2Le8leq0S6G_R5VEVTNK9A#Release-prompt-options. - return true; - }); - } -} - -/** - * @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 - */ -/** Computes the new pre-release version for the next release-train. */ -function computeNewPrereleaseVersionForNext(active, config) { - return tslib.__awaiter(this, void 0, void 0, function* () { - const { version: nextVersion } = active.next; - const isNextPublishedToNpm = yield isVersionPublishedToNpm(nextVersion, config); - // Special-case where the version in the `next` release-train is not published yet. This - // happens when we recently branched off for feature-freeze. We already bump the version to - // the next minor or major, but do not publish immediately. Cutting a release immediately would - // be not helpful as there are no other changes than in the feature-freeze branch. If we happen - // to detect this case, we stage the release as usual but do not increment the version. - if (isNextPublishedToNpm) { - return semverInc(nextVersion, 'prerelease'); - } - else { - return nextVersion; - } - }); -} - -/** - * @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 - */ -/** - * Release action that cuts a prerelease for the next branch. A version in the next - * branch can have an arbitrary amount of next pre-releases. - */ -class CutNextPrereleaseAction extends ReleaseAction { - constructor() { - super(...arguments); - /** Promise resolving with the new version if a NPM next pre-release is cut. */ - this._newVersion = this._computeNewVersion(); - } - getDescription() { - return tslib.__awaiter(this, void 0, void 0, function* () { - const { branchName } = this._getActivePrereleaseTrain(); - const newVersion = yield this._newVersion; - return `Cut a new next pre-release for the "${branchName}" branch (v${newVersion}).`; - }); - } - perform() { - return tslib.__awaiter(this, void 0, void 0, function* () { - const releaseTrain = this._getActivePrereleaseTrain(); - const { branchName } = releaseTrain; - const newVersion = yield this._newVersion; - const { pullRequest, releaseNotes } = yield this.checkoutBranchAndStageVersion(newVersion, branchName); - yield this.waitForPullRequestToBeMerged(pullRequest); - yield this.buildAndPublish(releaseNotes, branchName, 'next'); - // If the pre-release has been cut from a branch that is not corresponding - // to the next release-train, cherry-pick the changelog into the primary - // development branch. i.e. the `next` branch that is usually `master`. - if (releaseTrain !== this.active.next) { - yield this.cherryPickChangelogIntoNextBranch(releaseNotes, branchName); - } - }); - } - /** Gets the release train for which NPM next pre-releases should be cut. */ - _getActivePrereleaseTrain() { - var _a; - return (_a = this.active.releaseCandidate) !== null && _a !== void 0 ? _a : this.active.next; - } - /** Gets the new pre-release version for this release action. */ - _computeNewVersion() { - return tslib.__awaiter(this, void 0, void 0, function* () { - const releaseTrain = this._getActivePrereleaseTrain(); - // If a pre-release is cut for the next release-train, the new version is computed - // with respect to special cases surfacing with FF/RC branches. Otherwise, the basic - // pre-release increment of the version is used as new version. - if (releaseTrain === this.active.next) { - return yield computeNewPrereleaseVersionForNext(this.active, this.config); - } - else { - return semverInc(releaseTrain.version, 'prerelease'); - } - }); - } - static isActive() { - return tslib.__awaiter(this, void 0, void 0, function* () { - // Pre-releases for the `next` NPM dist tag can always be cut. Depending on whether - // there is a feature-freeze/release-candidate branch, the next pre-releases are either - // cut from such a branch, or from the actual `next` release-train branch (i.e. master). - return true; - }); - } -} - -/** - * @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 - */ -/** - * Cuts the first release candidate for a release-train currently in the - * feature-freeze phase. The version is bumped from `next` to `rc.0`. - */ -class CutReleaseCandidateForFeatureFreezeAction extends ReleaseAction { - constructor() { - super(...arguments); - this._newVersion = semverInc(this.active.releaseCandidate.version, 'prerelease', 'rc'); - } - getDescription() { - return tslib.__awaiter(this, void 0, void 0, function* () { - const newVersion = this._newVersion; - return `Cut a first release-candidate for the feature-freeze branch (v${newVersion}).`; - }); - } - perform() { - return tslib.__awaiter(this, void 0, void 0, function* () { - const { branchName } = this.active.releaseCandidate; - const newVersion = this._newVersion; - const { pullRequest, releaseNotes } = yield this.checkoutBranchAndStageVersion(newVersion, branchName); - yield this.waitForPullRequestToBeMerged(pullRequest); - yield this.buildAndPublish(releaseNotes, branchName, 'next'); - yield this.cherryPickChangelogIntoNextBranch(releaseNotes, branchName); - }); - } - static isActive(active) { - return tslib.__awaiter(this, void 0, void 0, function* () { - // A release-candidate can be cut for an active release-train currently - // in the feature-freeze phase. - return active.releaseCandidate !== null && - active.releaseCandidate.version.prerelease[0] === 'next'; - }); - } -} - -/** - * @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 - */ -/** - * Release action that cuts a stable version for the current release-train in the release - * candidate phase. The pre-release release-candidate version label is removed. - */ -class CutStableAction extends ReleaseAction { - constructor() { - super(...arguments); - this._newVersion = this._computeNewVersion(); - } - getDescription() { - return tslib.__awaiter(this, void 0, void 0, function* () { - const newVersion = this._newVersion; - return `Cut a stable release for the release-candidate branch (v${newVersion}).`; - }); - } - perform() { - var _a; - return tslib.__awaiter(this, void 0, void 0, function* () { - const { branchName } = this.active.releaseCandidate; - const newVersion = this._newVersion; - const isNewMajor = (_a = this.active.releaseCandidate) === null || _a === void 0 ? void 0 : _a.isMajor; - const { pullRequest, releaseNotes } = yield this.checkoutBranchAndStageVersion(newVersion, branchName); - yield this.waitForPullRequestToBeMerged(pullRequest); - // If a new major version is published, we publish to the `next` NPM dist tag temporarily. - // We do this because for major versions, we want all main Angular projects to have their - // new major become available at the same time. Publishing immediately to the `latest` NPM - // dist tag could cause inconsistent versions when users install packages with `@latest`. - // For example: Consider Angular Framework releases v12. CLI and Components would need to - // wait for that release to complete. Once done, they can update their dependencies to point - // to v12. Afterwards they could start the release process. In the meanwhile though, the FW - // dependencies were already available as `@latest`, so users could end up installing v12 while - // still having the older (but currently still latest) CLI version that is incompatible. - // The major release can be re-tagged to `latest` through a separate release action. - yield this.buildAndPublish(releaseNotes, branchName, isNewMajor ? 'next' : 'latest'); - // If a new major version is published and becomes the "latest" release-train, we need - // to set the LTS npm dist tag for the previous latest release-train (the current patch). - if (isNewMajor) { - const previousPatch = this.active.latest; - const ltsTagForPatch = getLtsNpmDistTagOfMajor(previousPatch.version.major); - // Instead of directly setting the NPM dist tags, we invoke the ng-dev command for - // setting the NPM dist tag to the specified version. We do this because release NPM - // packages could be different in the previous patch branch, and we want to set the - // LTS tag for all packages part of the last major. It would not be possible to set the - // NPM dist tag for new packages part of the released major, nor would it be acceptable - // to skip the LTS tag for packages which are no longer part of the new major. - yield this.checkoutUpstreamBranch(previousPatch.branchName); - yield invokeYarnInstallCommand(this.projectDir); - yield invokeSetNpmDistCommand(ltsTagForPatch, previousPatch.version); - } - yield this.cherryPickChangelogIntoNextBranch(releaseNotes, branchName); - }); - } - /** Gets the new stable version of the release candidate release-train. */ - _computeNewVersion() { - const { version } = this.active.releaseCandidate; - return semver.parse(`${version.major}.${version.minor}.${version.patch}`); - } - static isActive(active) { - return tslib.__awaiter(this, void 0, void 0, function* () { - // A stable version can be cut for an active release-train currently in the - // release-candidate phase. Note: It is not possible to directly release from - // feature-freeze phase into a stable version. - return active.releaseCandidate !== null && - active.releaseCandidate.version.prerelease[0] === 'rc'; - }); - } -} - -/** - * @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 - */ -/** - * Base action that can be used to move the next release-train into the feature-freeze or - * release-candidate phase. This means that a new version branch is created from the next - * branch, and a new pre-release (either RC or another `next`) is cut indicating the new phase. - */ -class BranchOffNextBranchBaseAction extends ReleaseAction { - getDescription() { - return tslib.__awaiter(this, void 0, void 0, function* () { - const { branchName } = this.active.next; - const newVersion = yield this._computeNewVersion(); - return `Move the "${branchName}" branch into ${this.newPhaseName} phase (v${newVersion}).`; - }); - } - perform() { - return tslib.__awaiter(this, void 0, void 0, function* () { - const newVersion = yield this._computeNewVersion(); - const newBranch = `${newVersion.major}.${newVersion.minor}.x`; - // Branch-off the next branch into a new version branch. - yield this._createNewVersionBranchFromNext(newBranch); - // Stage the new version for the newly created branch, and push changes to a - // fork in order to create a staging pull request. Note that we re-use the newly - // created branch instead of re-fetching from the upstream. - const { pullRequest, releaseNotes } = yield this.stageVersionForBranchAndCreatePullRequest(newVersion, newBranch); - // Wait for the staging PR to be merged. Then build and publish the feature-freeze next - // pre-release. Finally, cherry-pick the release notes into the next branch in combination - // with bumping the version to the next minor too. - yield this.waitForPullRequestToBeMerged(pullRequest); - yield this.buildAndPublish(releaseNotes, newBranch, 'next'); - yield this._createNextBranchUpdatePullRequest(releaseNotes, newVersion); - }); - } - /** Computes the new version for the release-train being branched-off. */ - _computeNewVersion() { - return tslib.__awaiter(this, void 0, void 0, function* () { - if (this.newPhaseName === 'feature-freeze') { - return computeNewPrereleaseVersionForNext(this.active, this.config); - } - else { - return semverInc(this.active.next.version, 'prerelease', 'rc'); - } - }); - } - /** Creates a new version branch from the next branch. */ - _createNewVersionBranchFromNext(newBranch) { - return tslib.__awaiter(this, void 0, void 0, function* () { - const { branchName: nextBranch } = this.active.next; - yield this.verifyPassingGithubStatus(nextBranch); - yield this.checkoutUpstreamBranch(nextBranch); - yield this.createLocalBranchFromHead(newBranch); - yield this.pushHeadToRemoteBranch(newBranch); - info(green(` ✓ Version branch "${newBranch}" created.`)); - }); - } - /** - * Creates a pull request for the next branch that bumps the version to the next - * minor, and cherry-picks the changelog for the newly branched-off release-candidate - * or feature-freeze version. - */ - _createNextBranchUpdatePullRequest(releaseNotes, newVersion) { - return tslib.__awaiter(this, void 0, void 0, function* () { - const { branchName: nextBranch, version } = this.active.next; - // We increase the version for the next branch to the next minor. The team can decide - // later if they want next to be a major through the `Configure Next as Major` release action. - const newNextVersion = semver.parse(`${version.major}.${version.minor + 1}.0-next.0`); - const bumpCommitMessage = getCommitMessageForExceptionalNextVersionBump(newNextVersion); - yield this.checkoutUpstreamBranch(nextBranch); - yield this.updateProjectVersion(newNextVersion); - // Create an individual commit for the next version bump. The changelog should go into - // a separate commit that makes it clear where the changelog is cherry-picked from. - yield this.createCommit(bumpCommitMessage, [packageJsonPath]); - yield this.prependReleaseNotesToChangelog(releaseNotes); - const commitMessage = getReleaseNoteCherryPickCommitMessage(releaseNotes.version); - yield this.createCommit(commitMessage, [changelogPath]); - let nextPullRequestMessage = `The previous "next" release-train has moved into the ` + - `${this.newPhaseName} phase. This PR updates the next branch to the subsequent ` + - `release-train.\n\nAlso this PR cherry-picks the changelog for ` + - `v${newVersion} into the ${nextBranch} branch so that the changelog is up to date.`; - const nextUpdatePullRequest = yield this.pushChangesToForkAndCreatePullRequest(nextBranch, `next-release-train-${newNextVersion}`, `Update next branch to reflect new release-train "v${newNextVersion}".`, nextPullRequestMessage); - info(green(` ✓ Pull request for updating the "${nextBranch}" branch has been created.`)); - info(yellow(` Please ask team members to review: ${nextUpdatePullRequest.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 - */ -/** - * Release action that moves the next release-train into the feature-freeze phase. This means - * that a new version branch is created from the next branch, and a new next pre-release is - * cut indicating the started feature-freeze. - */ -class MoveNextIntoFeatureFreezeAction extends BranchOffNextBranchBaseAction { - constructor() { - super(...arguments); - this.newPhaseName = 'feature-freeze'; - } - static isActive(active) { - return tslib.__awaiter(this, void 0, void 0, function* () { - // A new feature-freeze branch can only be created if there is no active - // release-train in feature-freeze/release-candidate phase and the version - // currently in the `next` branch is for a major. The feature-freeze phase - // is not foreseen for minor versions. - return active.releaseCandidate === null && active.next.isMajor; - }); - } -} - -/** - * @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 - */ -/** - * Release action that moves the next release-train into the release-candidate phase. This means - * that a new version branch is created from the next branch, and the first release candidate - * version is cut indicating the new phase. - */ -class MoveNextIntoReleaseCandidateAction extends BranchOffNextBranchBaseAction { - constructor() { - super(...arguments); - this.newPhaseName = 'release-candidate'; - } - static isActive(active) { - return tslib.__awaiter(this, void 0, void 0, function* () { - // Directly switching a next release-train into the `release-candidate` - // phase is only allowed for minor releases. Major version always need to - // go through the `feature-freeze` phase. - return active.releaseCandidate === null && !active.next.isMajor; - }); - } -} - -/** - * @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 - */ -/** - * Release action that tags the recently published major as latest within the NPM - * registry. Major versions are published to the `next` NPM dist tag initially and - * can be re-tagged to the `latest` NPM dist tag. This allows caretakers to make major - * releases available at the same time. e.g. Framework, Tooling and Components - * are able to publish v12 to `@latest` at the same time. This wouldn't be possible if - * we directly publish to `@latest` because Tooling and Components needs to wait - * for the major framework release to be available on NPM. - * @see {CutStableAction#perform} for more details. - */ -class TagRecentMajorAsLatest extends ReleaseAction { - getDescription() { - return tslib.__awaiter(this, void 0, void 0, function* () { - return `Tag recently published major v${this.active.latest.version} as "next" in NPM.`; - }); - } - perform() { - return tslib.__awaiter(this, void 0, void 0, function* () { - yield this.checkoutUpstreamBranch(this.active.latest.branchName); - yield invokeYarnInstallCommand(this.projectDir); - yield invokeSetNpmDistCommand('latest', this.active.latest.version); - }); - } - static isActive({ latest }, config) { - return tslib.__awaiter(this, void 0, void 0, function* () { - // If the latest release-train does currently not have a major version as version. e.g. - // the latest branch is `10.0.x` with the version being `10.0.2`. In such cases, a major - // has not been released recently, and this action should never become active. - if (latest.version.minor !== 0 || latest.version.patch !== 0) { - return false; - } - const packageInfo = yield fetchProjectNpmPackageInfo(config); - const npmLatestVersion = semver.parse(packageInfo['dist-tags']['latest']); - // This action only becomes active if a major just has been released recently, but is - // not set to the `latest` NPM dist tag in the NPM registry. Note that we only allow - // re-tagging if the current `@latest` in NPM is the previous major version. - return npmLatestVersion !== null && npmLatestVersion.major === latest.version.major - 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 - */ -/** - * List of release actions supported by the release staging tool. These are sorted - * by priority. Actions which are selectable are sorted based on this declaration order. - */ -const actions = [ - TagRecentMajorAsLatest, - CutStableAction, - CutReleaseCandidateForFeatureFreezeAction, - CutNewPatchAction, - CutNextPrereleaseAction, - MoveNextIntoFeatureFreezeAction, - MoveNextIntoReleaseCandidateAction, - CutLongTermSupportPatchAction, -]; - -/** - * @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 - */ -var CompletionState; -(function (CompletionState) { - CompletionState[CompletionState["SUCCESS"] = 0] = "SUCCESS"; - CompletionState[CompletionState["FATAL_ERROR"] = 1] = "FATAL_ERROR"; - CompletionState[CompletionState["MANUALLY_ABORTED"] = 2] = "MANUALLY_ABORTED"; -})(CompletionState || (CompletionState = {})); -class ReleaseTool { - constructor(_config, _github, _projectRoot) { - this._config = _config; - this._github = _github; - this._projectRoot = _projectRoot; - /** The singleton instance of the authenticated git client. */ - this._git = AuthenticatedGitClient.get(); - /** The previous git commit to return back to after the release tool runs. */ - this.previousGitBranchOrRevision = this._git.getCurrentBranchOrRevision(); - } - /** Runs the interactive release tool. */ - run() { - return tslib.__awaiter(this, void 0, void 0, function* () { - log(); - log(yellow('--------------------------------------------')); - log(yellow(' Angular Dev-Infra release staging script')); - log(yellow('--------------------------------------------')); - log(); - if (!(yield this._verifyEnvironmentHasPython3Symlink()) || - !(yield this._verifyNoUncommittedChanges()) || !(yield this._verifyRunningFromNextBranch())) { - return CompletionState.FATAL_ERROR; - } - if (!(yield this._verifyNpmLoginState())) { - return CompletionState.MANUALLY_ABORTED; - } - const { owner, name } = this._github; - const repo = { owner, name, api: this._git.github }; - const releaseTrains = yield fetchActiveReleaseTrains(repo); - // Print the active release trains so that the caretaker can access - // the current project branching state without switching context. - yield printActiveReleaseTrains(releaseTrains, this._config); - const action = yield this._promptForReleaseAction(releaseTrains); - try { - yield action.perform(); - } - catch (e) { - if (e instanceof UserAbortedReleaseActionError) { - return CompletionState.MANUALLY_ABORTED; - } - // Only print the error message and stack if the error is not a known fatal release - // action error (for which we print the error gracefully to the console with colors). - if (!(e instanceof FatalReleaseActionError) && e instanceof Error) { - console.error(e); - } - return CompletionState.FATAL_ERROR; - } - finally { - yield this.cleanup(); - } - return CompletionState.SUCCESS; - }); - } - /** Run post release tool cleanups. */ - cleanup() { - return tslib.__awaiter(this, void 0, void 0, function* () { - // Return back to the git state from before the release tool ran. - this._git.checkout(this.previousGitBranchOrRevision, true); - // Ensure log out of NPM. - yield npmLogout(this._config.publishRegistry); - }); - } - /** Prompts the caretaker for a release action that should be performed. */ - _promptForReleaseAction(activeTrains) { - return tslib.__awaiter(this, void 0, void 0, function* () { - const choices = []; - // Find and instantiate all release actions which are currently valid. - for (let actionType of actions) { - if (yield actionType.isActive(activeTrains, this._config)) { - const action = new actionType(activeTrains, this._git, this._config, this._projectRoot); - choices.push({ name: yield action.getDescription(), value: action }); - } - } - info('Please select the type of release you want to perform.'); - const { releaseAction } = yield inquirer.prompt({ - name: 'releaseAction', - message: 'Please select an action:', - type: 'list', - choices, - }); - return releaseAction; - }); - } - /** - * Verifies that there are no uncommitted changes in the project. - * @returns a boolean indicating success or failure. - */ - _verifyNoUncommittedChanges() { - return tslib.__awaiter(this, void 0, void 0, function* () { - if (this._git.hasUncommittedChanges()) { - error(red(' ✘ There are changes which are not committed and should be discarded.')); - return false; - } - return true; - }); - } - /** - * Verifies that Python can be resolved within scripts and points to a compatible version. Python - * is required in Bazel actions as there can be tools (such as `skydoc`) that rely on it. - * @returns a boolean indicating success or failure. - */ - _verifyEnvironmentHasPython3Symlink() { - return tslib.__awaiter(this, void 0, void 0, function* () { - try { - // Note: We do not rely on `/usr/bin/env` but rather access the `env` binary directly as it - // should be part of the shell's `$PATH`. This is necessary for compatibility with Windows. - const pyVersion = yield spawn('env', ['python', '--version'], { mode: 'silent' }); - const version = pyVersion.stdout.trim() || pyVersion.stderr.trim(); - if (version.startsWith('Python 3.')) { - debug(`Local python version: ${version}`); - return true; - } - error(red(` ✘ \`/usr/bin/python\` is currently symlinked to "${version}", please update`)); - error(red(' the symlink to link instead to Python3')); - error(); - error(red(' Googlers: please run the following command to symlink python to python3:')); - error(red(' sudo ln -s /usr/bin/python3 /usr/bin/python')); - return false; - } - catch (_a) { - error(red(' ✘ `/usr/bin/python` does not exist, please ensure `/usr/bin/python` is')); - error(red(' symlinked to Python3.')); - error(); - error(red(' Googlers: please run the following command to symlink python to python3:')); - error(red(' sudo ln -s /usr/bin/python3 /usr/bin/python')); - } - return false; - }); - } - /** - * Verifies that the next branch from the configured repository is checked out. - * @returns a boolean indicating success or failure. - */ - _verifyRunningFromNextBranch() { - return tslib.__awaiter(this, void 0, void 0, function* () { - const headSha = this._git.run(['rev-parse', 'HEAD']).stdout.trim(); - const { data } = yield this._git.github.repos.getBranch(Object.assign(Object.assign({}, this._git.remoteParams), { branch: nextBranchName })); - if (headSha !== data.commit.sha) { - error(red(' ✘ Running release tool from an outdated local branch.')); - error(red(` Please make sure you are running from the "${nextBranchName}" branch.`)); - return false; - } - return true; - }); - } - /** - * Verifies that the user is logged into NPM at the correct registry, if defined for the release. - * @returns a boolean indicating whether the user is logged into NPM. - */ - _verifyNpmLoginState() { - var _a, _b; - return tslib.__awaiter(this, void 0, void 0, function* () { - const registry = `NPM at the ${(_a = this._config.publishRegistry) !== null && _a !== void 0 ? _a : 'default NPM'} registry`; - // TODO(josephperrott): remove wombat specific block once wombot allows `npm whoami` check to - // check the status of the local token in the .npmrc file. - if ((_b = this._config.publishRegistry) === null || _b === void 0 ? void 0 : _b.includes('wombat-dressing-room.appspot.com')) { - info('Unable to determine NPM login state for wombat proxy, requiring login now.'); - try { - yield npmLogin(this._config.publishRegistry); - } - catch (_c) { - return false; - } - return true; - } - if (yield npmIsLoggedIn(this._config.publishRegistry)) { - debug(`Already logged into ${registry}.`); - return true; - } - error(red(` ✘ Not currently logged into ${registry}.`)); - const shouldLogin = yield promptConfirm('Would you like to log into NPM now?'); - if (shouldLogin) { - debug('Starting NPM login.'); - try { - yield npmLogin(this._config.publishRegistry); - } - catch (_d) { - return false; - } - return true; - } - 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 - */ -/** Yargs command builder for configuring the `ng-dev release publish` command. */ -function builder$a(argv) { - return addGithubTokenOption(argv); -} -/** Yargs command handler for staging a release. */ -function handler$b() { - return tslib.__awaiter(this, void 0, void 0, function* () { - const git = GitClient.get(); - const config = getConfig(); - const releaseConfig = getReleaseConfig(config); - const projectDir = git.baseDir; - const task = new ReleaseTool(releaseConfig, config.github, projectDir); - const result = yield task.run(); - switch (result) { - case CompletionState.FATAL_ERROR: - error(red(`Release action has been aborted due to fatal errors. See above.`)); - process.exitCode = 2; - break; - case CompletionState.MANUALLY_ABORTED: - info(yellow(`Release action has been manually aborted.`)); - process.exitCode = 1; - break; - case CompletionState.SUCCESS: - info(green(`Release action has completed successfully.`)); - break; - } - }); -} -/** CLI command module for publishing a release. */ -const ReleasePublishCommandModule = { - builder: builder$a, - handler: handler$b, - command: 'publish', - describe: 'Publish new releases and configure version branches.', -}; - -/** - * @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 - */ -function builder$b(args) { - return args - .positional('tagName', { - type: 'string', - demandOption: true, - description: 'Name of the NPM dist tag.', - }) - .positional('targetVersion', { - type: 'string', - demandOption: true, - description: 'Version to which the dist tag should be set.' - }); -} -/** Yargs command handler for building a release. */ -function handler$c(args) { - return tslib.__awaiter(this, void 0, void 0, function* () { - const { targetVersion: rawVersion, tagName } = args; - const { npmPackages, publishRegistry } = getReleaseConfig(); - const version = semver.parse(rawVersion); - if (version === null) { - error(red(`Invalid version specified (${rawVersion}). Unable to set NPM dist tag.`)); - process.exit(1); - } - const spinner = ora.call(undefined).start(); - debug(`Setting "${tagName}" NPM dist tag for release packages to v${version}.`); - for (const pkgName of npmPackages) { - spinner.text = `Setting NPM dist tag for "${pkgName}"`; - spinner.render(); - try { - yield setNpmTagForPackage(pkgName, tagName, version, publishRegistry); - debug(`Successfully set "${tagName}" NPM dist tag for "${pkgName}".`); - } - catch (e) { - spinner.stop(); - error(e); - error(red(` ✘ An error occurred while setting the NPM dist tag for "${pkgName}".`)); - process.exit(1); - } - } - spinner.stop(); - info(green(` ✓ Set NPM dist tag for all release packages.`)); - info(green(` ${bold(tagName)} will now point to ${bold(`v${version}`)}.`)); - }); -} -/** CLI command module for setting an NPM dist tag. */ -const ReleaseSetDistTagCommand = { - builder: builder$b, - handler: handler$c, - command: 'set-dist-tag ', - describe: 'Sets a given NPM dist tag for all release packages.', -}; - -/** - * @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 - */ -/** - * Log the environment variables expected by bazel for stamping. - * - * See the section on stamping in docs / BAZEL.md - * - * This script must be a NodeJS script in order to be cross-platform. - * See https://github.com/bazelbuild/bazel/issues/5958 - * Note: git operations, especially git status, take a long time inside mounted docker volumes - * in Windows or OSX hosts (https://github.com/docker/for-win/issues/188). - */ -function buildEnvStamp(mode) { - console.info(`BUILD_SCM_BRANCH ${getCurrentBranch()}`); - console.info(`BUILD_SCM_COMMIT_SHA ${getCurrentBranchOrRevision()}`); - console.info(`BUILD_SCM_HASH ${getCurrentBranchOrRevision()}`); - console.info(`BUILD_SCM_LOCAL_CHANGES ${hasLocalChanges()}`); - console.info(`BUILD_SCM_USER ${getCurrentGitUser()}`); - const { version, experimentalVersion } = getSCMVersions(mode); - console.info(`BUILD_SCM_VERSION ${version}`); - console.info(`BUILD_SCM_EXPERIMENTAL_VERSION ${experimentalVersion}`); - process.exit(); -} -/** Whether the repo has local changes. */ -function hasLocalChanges() { - try { - const git = GitClient.get(); - return git.hasUncommittedChanges(); - } - catch (_a) { - return true; - } -} -/** - * Get the versions for generated packages. - * - * In snapshot mode, the version is based on the most recent semver tag. - * In release mode, the version is based on the base package.json version. - */ -function getSCMVersions(mode) { - try { - const git = GitClient.get(); - if (mode === 'snapshot') { - const localChanges = hasLocalChanges() ? '.with-local-changes' : ''; - const { stdout: rawVersion } = git.run(['describe', '--match', '*[0-9]*.[0-9]*.[0-9]*', '--abbrev=7', '--tags', 'HEAD']); - const { version } = new semver.SemVer(rawVersion); - const { version: experimentalVersion } = createExperimentalSemver(version); - return { - version: `${version.replace(/-([0-9]+)-g/, '+$1.sha-')}${localChanges}`, - experimentalVersion: `${experimentalVersion.replace(/-([0-9]+)-g/, '+$1.sha-')}${localChanges}`, - }; - } - else { - const packageJsonPath = path.join(git.baseDir, 'package.json'); - const { version } = new semver.SemVer(require(packageJsonPath).version); - const { version: experimentalVersion } = createExperimentalSemver(new semver.SemVer(version)); - return { version, experimentalVersion }; - } - } - catch (_a) { - return { - version: '', - experimentalVersion: '', - }; - } -} -/** Get the current branch or revision of HEAD. */ -function getCurrentBranchOrRevision() { - try { - const git = GitClient.get(); - return git.getCurrentBranchOrRevision(); - } - catch (_a) { - return ''; - } -} -/** Get the currently checked out branch. */ -function getCurrentBranch() { - try { - const git = GitClient.get(); - return git.run(['symbolic-ref', '--short', 'HEAD']).stdout.trim(); - } - catch (_a) { - return ''; - } -} -/** Get the current git user based on the git config. */ -function getCurrentGitUser() { - try { - const git = GitClient.get(); - let userName = git.runGraceful(['config', 'user.name']).stdout.trim() || 'Unknown User'; - let userEmail = git.runGraceful(['config', 'user.email']).stdout.trim() || 'unknown_email'; - return `${userName} <${userEmail}>`; - } - catch (_a) { - return ''; - } -} - -/** - * @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 - */ -function builder$c(args) { - return args.option('mode', { - demandOption: true, - description: 'Whether the env-stamp should be built for a snapshot or release', - choices: ['snapshot', 'release'] - }); -} -function handler$d({ mode }) { - return tslib.__awaiter(this, void 0, void 0, function* () { - buildEnvStamp(mode); - }); -} -/** CLI command module for building the environment stamp. */ -const BuildEnvStampCommand = { - builder: builder$c, - handler: handler$d, - command: 'build-env-stamp', - describe: 'Build the environment stamping information', -}; - -/** Build the parser for the release commands. */ -function buildReleaseParser(localYargs) { - return localYargs.help() - .strict() - .demandCommand() - .command(ReleasePublishCommandModule) - .command(ReleaseBuildCommandModule) - .command(ReleaseInfoCommandModule) - .command(ReleaseSetDistTagCommand) - .command(BuildEnvStampCommand) - .command(ReleaseNotesCommandModule); -} - -/** - * @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 - */ -/** Gets the status of the specified file. Returns null if the file does not exist. */ -function getFileStatus(filePath) { - try { - return fs.statSync(filePath); - } - catch (_a) { - return null; - } -} -/** Ensures that the specified path uses forward slashes as delimiter. */ -function convertPathToForwardSlash(path) { - return path.replace(/\\/g, '/'); -} - -/** - * @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 - */ -/** - * Finds all module references in the specified source file. - * @param node Source file which should be parsed. - * @returns List of import specifiers in the source file. - */ -function getModuleReferences(node) { - const references = []; - const visitNode = (node) => { - if ((ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) && - node.moduleSpecifier !== undefined && ts.isStringLiteral(node.moduleSpecifier)) { - references.push(node.moduleSpecifier.text); - } - ts.forEachChild(node, visitNode); - }; - ts.forEachChild(node, visitNode); - return references; -} - -/** - * @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 - */ -/** Default extensions that the analyzer uses for resolving imports. */ -const DEFAULT_EXTENSIONS = ['ts', 'js', 'd.ts']; -/** - * Analyzer that can be used to detect import cycles within source files. It supports - * custom module resolution, source file caching and collects unresolved specifiers. - */ -class Analyzer { - constructor(resolveModuleFn, extensions = DEFAULT_EXTENSIONS) { - this.resolveModuleFn = resolveModuleFn; - this.extensions = extensions; - this._sourceFileCache = new Map(); - this.unresolvedModules = new Set(); - this.unresolvedFiles = new Map(); - } - /** Finds all cycles in the specified source file. */ - findCycles(sf, visited = new WeakSet(), path = []) { - const previousIndex = path.indexOf(sf); - // If the given node is already part of the current path, then a cycle has - // been found. Add the reference chain which represents the cycle to the results. - if (previousIndex !== -1) { - return [path.slice(previousIndex)]; - } - // If the node has already been visited, then it's not necessary to go check its edges - // again. Cycles would have been already detected and collected in the first check. - if (visited.has(sf)) { - return []; - } - path.push(sf); - visited.add(sf); - // Go through all edges, which are determined through import/exports, and collect cycles. - const result = []; - for (const ref of getModuleReferences(sf)) { - const targetFile = this._resolveImport(ref, sf.fileName); - if (targetFile !== null) { - result.push(...this.findCycles(this.getSourceFile(targetFile), visited, path.slice())); - } - } - return result; - } - /** Gets the TypeScript source file of the specified path. */ - getSourceFile(filePath) { - const resolvedPath = path.resolve(filePath); - if (this._sourceFileCache.has(resolvedPath)) { - return this._sourceFileCache.get(resolvedPath); - } - const fileContent = fs.readFileSync(resolvedPath, 'utf8'); - const sourceFile = ts.createSourceFile(resolvedPath, fileContent, ts.ScriptTarget.Latest, false); - this._sourceFileCache.set(resolvedPath, sourceFile); - return sourceFile; - } - /** Resolves the given import specifier with respect to the specified containing file path. */ - _resolveImport(specifier, containingFilePath) { - if (specifier.charAt(0) === '.') { - const resolvedPath = this._resolveFileSpecifier(specifier, containingFilePath); - if (resolvedPath === null) { - this._trackUnresolvedFileImport(specifier, containingFilePath); - } - return resolvedPath; - } - if (this.resolveModuleFn) { - const targetFile = this.resolveModuleFn(specifier); - if (targetFile !== null) { - const resolvedPath = this._resolveFileSpecifier(targetFile); - if (resolvedPath !== null) { - return resolvedPath; - } - } - } - this.unresolvedModules.add(specifier); - return null; - } - /** Tracks the given file import as unresolved. */ - _trackUnresolvedFileImport(specifier, originFilePath) { - if (!this.unresolvedFiles.has(originFilePath)) { - this.unresolvedFiles.set(originFilePath, [specifier]); - } - this.unresolvedFiles.get(originFilePath).push(specifier); - } - /** Resolves the given import specifier to the corresponding source file. */ - _resolveFileSpecifier(specifier, containingFilePath) { - const importFullPath = containingFilePath !== undefined ? path.join(path.dirname(containingFilePath), specifier) : specifier; - const stat = getFileStatus(importFullPath); - if (stat && stat.isFile()) { - return importFullPath; - } - for (const extension of this.extensions) { - const pathWithExtension = `${importFullPath}.${extension}`; - const stat = getFileStatus(pathWithExtension); - if (stat && stat.isFile()) { - return pathWithExtension; - } - } - // Directories should be considered last. TypeScript first looks for source files, then - // falls back to directories if no file with appropriate extension could be found. - if (stat && stat.isDirectory()) { - return this._resolveFileSpecifier(path.join(importFullPath, 'index')); - } - return null; - } -} - -/** - * @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 - */ -/** - * Loads the configuration for the circular dependencies test. If the config cannot be - * loaded, an error will be printed and the process exists with a non-zero exit code. - */ -function loadTestConfig(configPath) { - const configBaseDir = path.dirname(configPath); - const resolveRelativePath = (relativePath) => path.resolve(configBaseDir, relativePath); - try { - const config = require(configPath); - if (!path.isAbsolute(config.baseDir)) { - config.baseDir = resolveRelativePath(config.baseDir); - } - if (!path.isAbsolute(config.goldenFile)) { - config.goldenFile = resolveRelativePath(config.goldenFile); - } - if (!path.isAbsolute(config.glob)) { - config.glob = resolveRelativePath(config.glob); - } - return config; - } - catch (e) { - error('Could not load test configuration file at: ' + configPath); - error(`Failed with: ${e.message}`); - 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 - */ -/** - * Converts a list of reference chains to a JSON-compatible golden object. Reference chains - * by default use TypeScript source file objects. In order to make those chains printable, - * the source file objects are mapped to their relative file names. - */ -function convertReferenceChainToGolden(refs, baseDir) { - return refs - .map( - // Normalize cycles as the paths can vary based on which node in the cycle is visited - // first in the analyzer. The paths represent cycles. Hence we can shift nodes in a - // deterministic way so that the goldens don't change unnecessarily and cycle comparison - // is simpler. - chain => normalizeCircularDependency(chain.map(({ fileName }) => convertPathToForwardSlash(path.relative(baseDir, fileName))))) - // Sort cycles so that the golden doesn't change unnecessarily when cycles are detected - // in different order (e.g. new imports cause cycles to be detected earlier or later). - .sort(compareCircularDependency); -} -/** - * Compares the specified goldens and returns two lists that describe newly - * added circular dependencies, or fixed circular dependencies. - */ -function compareGoldens(actual, expected) { - const newCircularDeps = []; - const fixedCircularDeps = []; - actual.forEach(a => { - if (!expected.find(e => isSameCircularDependency(a, e))) { - newCircularDeps.push(a); - } - }); - expected.forEach(e => { - if (!actual.find(a => isSameCircularDependency(e, a))) { - fixedCircularDeps.push(e); - } - }); - return { newCircularDeps, fixedCircularDeps }; -} -/** - * Normalizes the a circular dependency by ensuring that the path starts with the first - * node in alphabetical order. Since the path array represents a cycle, we can make a - * specific node the first element in the path that represents the cycle. - * - * This method is helpful because the path of circular dependencies changes based on which - * file in the path has been visited first by the analyzer. e.g. Assume we have a circular - * dependency represented as: `A -> B -> C`. The analyzer will detect this cycle when it - * visits `A`. Though when a source file that is analyzed before `A` starts importing `B`, - * the cycle path will detected as `B -> C -> A`. This represents the same cycle, but is just - * different due to a limitation of using a data structure that can be written to a text-based - * golden file. - * - * To account for this non-deterministic behavior in goldens, we shift the circular - * dependency path to the first node based on alphabetical order. e.g. `A` will always - * be the first node in the path that represents the cycle. - */ -function normalizeCircularDependency(path) { - if (path.length <= 1) { - return path; - } - let indexFirstNode = 0; - let valueFirstNode = path[0]; - // Find a node in the cycle path that precedes all other elements - // in terms of alphabetical order. - for (let i = 1; i < path.length; i++) { - const value = path[i]; - if (value.localeCompare(valueFirstNode, 'en') < 0) { - indexFirstNode = i; - valueFirstNode = value; - } - } - // If the alphabetically first node is already at start of the path, just - // return the actual path as no changes need to be made. - if (indexFirstNode === 0) { - return path; - } - // Move the determined first node (as of alphabetical order) to the start of a new - // path array. The nodes before the first node in the old path are then concatenated - // to the end of the new path. This is possible because the path represents a cycle. - return [...path.slice(indexFirstNode), ...path.slice(0, indexFirstNode)]; -} -/** Checks whether the specified circular dependencies are equal. */ -function isSameCircularDependency(actual, expected) { - if (actual.length !== expected.length) { - return false; - } - for (let i = 0; i < actual.length; i++) { - if (actual[i] !== expected[i]) { - return false; - } - } - return true; -} -/** - * Compares two circular dependencies by respecting the alphabetic order of nodes in the - * cycle paths. The first nodes which don't match in both paths are decisive on the order. - */ -function compareCircularDependency(a, b) { - // Go through nodes in both cycle paths and determine whether `a` should be ordered - // before `b`. The first nodes which don't match decide on the order. - for (let i = 0; i < Math.min(a.length, b.length); i++) { - const compareValue = a[i].localeCompare(b[i], 'en'); - if (compareValue !== 0) { - return compareValue; - } - } - // If all nodes are equal in the cycles, the order is based on the length of both cycles. - return a.length - b.length; -} - -/** - * @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 - */ -function tsCircularDependenciesBuilder(localYargs) { - return localYargs.help() - .strict() - .demandCommand() - .option('config', { type: 'string', demandOption: true, description: 'Path to the configuration file.' }) - .option('warnings', { type: 'boolean', description: 'Prints all warnings.' }) - .command('check', 'Checks if the circular dependencies have changed.', args => args, argv => { - const { config: configArg, warnings } = argv; - const configPath = path.isAbsolute(configArg) ? configArg : path.resolve(configArg); - const config = loadTestConfig(configPath); - process.exit(main(false, config, !!warnings)); - }) - .command('approve', 'Approves the current circular dependencies.', args => args, argv => { - const { config: configArg, warnings } = argv; - const configPath = path.isAbsolute(configArg) ? configArg : path.resolve(configArg); - const config = loadTestConfig(configPath); - process.exit(main(true, config, !!warnings)); - }); -} -/** - * Runs the ts-circular-dependencies tool. - * @param approve Whether the detected circular dependencies should be approved. - * @param config Configuration for the current circular dependencies test. - * @param printWarnings Whether warnings should be printed out. - * @returns Status code. - */ -function main(approve, config, printWarnings) { - const { baseDir, goldenFile, glob: glob$1, resolveModule, approveCommand } = config; - const analyzer = new Analyzer(resolveModule); - const cycles = []; - const checkedNodes = new WeakSet(); - glob.sync(glob$1, { absolute: true, ignore: ['**/node_modules/**'] }).forEach(filePath => { - const sourceFile = analyzer.getSourceFile(filePath); - cycles.push(...analyzer.findCycles(sourceFile, checkedNodes)); - }); - const actual = convertReferenceChainToGolden(cycles, baseDir); - info(green(` Current number of cycles: ${yellow(cycles.length.toString())}`)); - if (approve) { - fs.writeFileSync(goldenFile, JSON.stringify(actual, null, 2)); - info(green('✅ Updated golden file.')); - return 0; - } - else if (!fs.existsSync(goldenFile)) { - error(red(`❌ Could not find golden file: ${goldenFile}`)); - return 1; - } - const warningsCount = analyzer.unresolvedFiles.size + analyzer.unresolvedModules.size; - // By default, warnings for unresolved files or modules are not printed. This is because - // it's common that third-party modules are not resolved/visited. Also generated files - // from the View Engine compiler (i.e. factories, summaries) cannot be resolved. - if (printWarnings && warningsCount !== 0) { - info(yellow('⚠ The following imports could not be resolved:')); - Array.from(analyzer.unresolvedModules).sort().forEach(specifier => info(` • ${specifier}`)); - analyzer.unresolvedFiles.forEach((value, key) => { - info(` • ${getRelativePath(baseDir, key)}`); - value.sort().forEach(specifier => info(` ${specifier}`)); - }); - } - else { - info(yellow(`⚠ ${warningsCount} imports could not be resolved.`)); - info(yellow(` Please rerun with "--warnings" to inspect unresolved imports.`)); - } - const expected = JSON.parse(fs.readFileSync(goldenFile, 'utf8')); - const { fixedCircularDeps, newCircularDeps } = compareGoldens(actual, expected); - const isMatching = fixedCircularDeps.length === 0 && newCircularDeps.length === 0; - if (isMatching) { - info(green('✅ Golden matches current circular dependencies.')); - return 0; - } - error(red('❌ Golden does not match current circular dependencies.')); - if (newCircularDeps.length !== 0) { - error(yellow(` New circular dependencies which are not allowed:`)); - newCircularDeps.forEach(c => error(` • ${convertReferenceChainToString(c)}`)); - error(); - } - if (fixedCircularDeps.length !== 0) { - error(yellow(` Fixed circular dependencies that need to be removed from the golden:`)); - fixedCircularDeps.forEach(c => error(` • ${convertReferenceChainToString(c)}`)); - info(yellow(`\n Total: ${newCircularDeps.length} new cycle(s), ${fixedCircularDeps.length} fixed cycle(s). \n`)); - if (approveCommand) { - info(yellow(` Please approve the new golden with: ${approveCommand}`)); - } - else { - info(yellow(` Please update the golden. The following command can be ` + - `run: yarn ts-circular-deps approve ${getRelativePath(process.cwd(), goldenFile)}.`)); - } - } - return 1; -} -/** Gets the specified path relative to the base directory. */ -function getRelativePath(baseDir, path$1) { - return convertPathToForwardSlash(path.relative(baseDir, path$1)); -} -/** Converts the given reference chain to its string representation. */ -function convertReferenceChainToString(chain) { - return chain.join(' → '); -} - -/** - * @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 - */ -/** Yargs command builder for the command. */ -function builder$d(argv) { - return argv.positional('projectRoot', { - type: 'string', - normalize: true, - coerce: (path$1) => path.resolve(path$1), - demandOption: true, - }); -} -/** Yargs command handler for the command. */ -function handler$e({ projectRoot }) { - return tslib.__awaiter(this, void 0, void 0, function* () { - try { - if (!fs.lstatSync(projectRoot).isDirectory()) { - error(red(` ✘ The 'projectRoot' must be a directory: ${projectRoot}`)); - process.exit(1); - } - } - catch (_a) { - error(red(` ✘ Could not find the 'projectRoot' provided: ${projectRoot}`)); - process.exit(1); - } - const releaseOutputs = yield buildReleaseOutput(false); - if (releaseOutputs === null) { - error(red(` ✘ Could not build release output. Please check output above.`)); - process.exit(1); - } - info(chalk.green(` ✓ Built release output.`)); - for (const { outputPath, name } of releaseOutputs) { - yield spawn('yarn', ['link', '--cwd', outputPath]); - yield spawn('yarn', ['link', '--cwd', projectRoot, name]); - } - info(chalk.green(` ✓ Linked release packages in provided project.`)); - }); -} -/** CLI command module. */ -const BuildAndLinkCommandModule = { - builder: builder$d, - handler: handler$e, - command: 'build-and-link ', - describe: 'Builds the release output, registers the outputs as linked, and links via yarn to the provided project', -}; - -/** Build the parser for the misc commands. */ -function buildMiscParser(localYargs) { - return localYargs.help().strict().command(BuildAndLinkCommandModule); -} - -yargs.scriptName('ng-dev') - .middleware(captureLogOutputForCommand) - .demandCommand() - .recommendCommands() - .command('commit-message ', '', buildCommitMessageParser) - .command('format ', '', buildFormatParser) - .command('pr ', '', buildPrParser) - .command('pullapprove ', '', buildPullapproveParser) - .command('release ', '', buildReleaseParser) - .command('ts-circular-deps ', '', tsCircularDependenciesBuilder) - .command('caretaker ', '', buildCaretakerParser) - .command('misc ', '', buildMiscParser) - .command('ngbot ', false, buildNgbotParser) - .wrap(120) - .strict() - .parse(); diff --git a/dev-infra/ngbot/BUILD.bazel b/dev-infra/ngbot/BUILD.bazel deleted file mode 100644 index a22ccaa646f88c..00000000000000 --- a/dev-infra/ngbot/BUILD.bazel +++ /dev/null @@ -1,18 +0,0 @@ -load("//dev-infra:defaults.bzl", "ts_library") - -ts_library( - name = "ngbot", - srcs = [ - "cli.ts", - "verify.ts", - ], - visibility = ["//dev-infra:__subpackages__"], - deps = [ - "//dev-infra/utils", - "@npm//@types/node", - "@npm//@types/yaml", - "@npm//@types/yargs", - "@npm//yaml", - "@npm//yargs", - ], -) diff --git a/dev-infra/ngbot/cli.ts b/dev-infra/ngbot/cli.ts deleted file mode 100644 index e03b57926fb9d0..00000000000000 --- a/dev-infra/ngbot/cli.ts +++ /dev/null @@ -1,15 +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 {verify} from './verify'; - -/** Build the parser for the NgBot commands. */ -export function buildNgbotParser(localYargs: yargs.Argv) { - return localYargs.help().strict().demandCommand().command( - 'verify', 'Verify the NgBot config', {}, () => verify()); -} diff --git a/dev-infra/ngbot/verify.ts b/dev-infra/ngbot/verify.ts deleted file mode 100644 index af9e559efcb6cb..00000000000000 --- a/dev-infra/ngbot/verify.ts +++ /dev/null @@ -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 {readFileSync} from 'fs'; -import {resolve} from 'path'; -import {parse as parseYaml} from 'yaml'; - -import {error, green, info, red} from '../utils/console'; -import {GitClient} from '../utils/git/git-client'; - -export function verify() { - const git = GitClient.get(); - /** Full path to NgBot config file */ - const NGBOT_CONFIG_YAML_PATH = resolve(git.baseDir, '.github/angular-robot.yml'); - - /** The NgBot config file */ - const ngBotYaml = readFileSync(NGBOT_CONFIG_YAML_PATH, 'utf8'); - - try { - // Try parsing the config file to verify that the syntax is correct. - parseYaml(ngBotYaml); - info(`${green('√')} Valid NgBot YAML config`); - } catch (e) { - error(`${red('!')} Invalid NgBot YAML config`); - error(e); - process.exitCode = 1; - } -} diff --git a/dev-infra/pr/BUILD.bazel b/dev-infra/pr/BUILD.bazel deleted file mode 100644 index fd55fd837eeb1e..00000000000000 --- a/dev-infra/pr/BUILD.bazel +++ /dev/null @@ -1,15 +0,0 @@ -load("//dev-infra:defaults.bzl", "ts_library") - -ts_library( - name = "pr", - srcs = ["cli.ts"], - visibility = ["//dev-infra:__subpackages__"], - deps = [ - "//dev-infra/pr/check-target-branches", - "//dev-infra/pr/checkout", - "//dev-infra/pr/discover-new-conflicts", - "//dev-infra/pr/merge", - "//dev-infra/pr/rebase", - "@npm//@types/yargs", - ], -) diff --git a/dev-infra/pr/check-target-branches/BUILD.bazel b/dev-infra/pr/check-target-branches/BUILD.bazel deleted file mode 100644 index 7b33c529be91a8..00000000000000 --- a/dev-infra/pr/check-target-branches/BUILD.bazel +++ /dev/null @@ -1,12 +0,0 @@ -load("//dev-infra:defaults.bzl", "ts_library") - -ts_library( - name = "check-target-branches", - srcs = glob(["*.ts"]), - visibility = ["//dev-infra:__subpackages__"], - deps = [ - "//dev-infra/pr/merge", - "//dev-infra/utils", - "@npm//@types/yargs", - ], -) diff --git a/dev-infra/pr/check-target-branches/check-target-branches.ts b/dev-infra/pr/check-target-branches/check-target-branches.ts deleted file mode 100644 index 8b151c5b6d84bb..00000000000000 --- a/dev-infra/pr/check-target-branches/check-target-branches.ts +++ /dev/null @@ -1,63 +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 {getConfig} from '../../utils/config'; -import {error, info, red} from '../../utils/console'; -import {GitClient} from '../../utils/git/git-client'; -import {loadAndValidateConfig, TargetLabel} from '../merge/config'; -import {getBranchesFromTargetLabel, getTargetLabelFromPullRequest, InvalidTargetLabelError} from '../merge/target-label'; - -export async function getTargetBranchesForPr(prNumber: number) { - /** The ng-dev configuration. */ - const config = getConfig(); - /** Repo owner and name for the github repository. */ - const {owner, name: repo} = config.github; - /** The singleton instance of the GitClient. */ - const git = GitClient.get(); - /** The validated merge config. */ - const {config: mergeConfig, errors} = await loadAndValidateConfig(config, git.github); - if (errors !== undefined) { - throw Error(`Invalid configuration found: ${errors}`); - } - /** The current state of the pull request from Github. */ - const prData = (await git.github.pulls.get({owner, repo, pull_number: prNumber})).data; - /** The list of labels on the PR as strings. */ - // Note: The `name` property of labels is always set but the Github OpenAPI spec is incorrect - // here. - // TODO(devversion): Remove the non-null cast once - // https://github.com/github/rest-api-description/issues/169 is fixed. - const labels = prData.labels.map(l => l.name!); - /** The branch targetted via the Github UI. */ - const githubTargetBranch = prData.base.ref; - /** The active label which is being used for targetting the PR. */ - let targetLabel: TargetLabel; - - try { - targetLabel = getTargetLabelFromPullRequest(mergeConfig!, labels); - } catch (e) { - if (e instanceof InvalidTargetLabelError) { - error(red(e.failureMessage)); - process.exitCode = 1; - return; - } - throw e; - } - /** The target branches based on the target label and branch targetted in the Github UI. */ - return await getBranchesFromTargetLabel(targetLabel, githubTargetBranch); -} - - -export async function printTargetBranchesForPr(prNumber: number) { - const targets = await getTargetBranchesForPr(prNumber); - if (targets === undefined) { - return; - } - info.group(`PR #${prNumber} will merge into:`); - targets.forEach(target => info(`- ${target}`)); - info.groupEnd(); -} diff --git a/dev-infra/pr/check-target-branches/cli.ts b/dev-infra/pr/check-target-branches/cli.ts deleted file mode 100644 index a00300378cb9d9..00000000000000 --- a/dev-infra/pr/check-target-branches/cli.ts +++ /dev/null @@ -1,37 +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 {printTargetBranchesForPr} from './check-target-branches'; - -export interface CheckTargetBranchesOptions { - pr: number; -} - -/** Builds the command. */ -function builder(yargs: Argv) { - return yargs.positional('pr', { - description: 'The pull request number', - type: 'number', - demandOption: true, - }); -} - -/** Handles the command. */ -async function handler({pr}: Arguments) { - await printTargetBranchesForPr(pr); -} - -/** yargs command module describing the command. */ -export const CheckTargetBranchesModule: CommandModule<{}, CheckTargetBranchesOptions> = { - handler, - builder, - command: 'check-target-branches ', - describe: 'Check a PR to determine what branches it is currently targeting', -}; diff --git a/dev-infra/pr/checkout/BUILD.bazel b/dev-infra/pr/checkout/BUILD.bazel deleted file mode 100644 index a1a2b3e3e24dff..00000000000000 --- a/dev-infra/pr/checkout/BUILD.bazel +++ /dev/null @@ -1,12 +0,0 @@ -load("//dev-infra:defaults.bzl", "ts_library") - -ts_library( - name = "checkout", - srcs = glob(["*.ts"]), - visibility = ["//dev-infra:__subpackages__"], - deps = [ - "//dev-infra/pr/common", - "//dev-infra/utils", - "@npm//@types/yargs", - ], -) diff --git a/dev-infra/pr/checkout/cli.ts b/dev-infra/pr/checkout/cli.ts deleted file mode 100644 index 1ea44bd937fefe..00000000000000 --- a/dev-infra/pr/checkout/cli.ts +++ /dev/null @@ -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 {Arguments, Argv, CommandModule} from 'yargs'; - -import {addGithubTokenOption} from '../../utils/git/github-yargs'; -import {checkOutPullRequestLocally} from '../common/checkout-pr'; - -export interface CheckoutOptions { - prNumber: number; - githubToken: string; -} - -/** Builds the checkout pull request command. */ -function builder(yargs: Argv) { - return addGithubTokenOption(yargs).positional('prNumber', {type: 'number', demandOption: true}); -} - -/** Handles the checkout pull request command. */ -async function handler({prNumber, githubToken}: Arguments) { - const prCheckoutOptions = {allowIfMaintainerCannotModify: true, branchName: `pr-${prNumber}`}; - await checkOutPullRequestLocally(prNumber, githubToken, prCheckoutOptions); -} - -/** yargs command module for checking out a PR */ -export const CheckoutCommandModule: CommandModule<{}, CheckoutOptions> = { - handler, - builder, - command: 'checkout ', - describe: 'Checkout a PR from the upstream repo', -}; diff --git a/dev-infra/pr/cli.ts b/dev-infra/pr/cli.ts deleted file mode 100644 index df57551dfabd98..00000000000000 --- a/dev-infra/pr/cli.ts +++ /dev/null @@ -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 yargs from 'yargs'; - -import {CheckTargetBranchesModule} from './check-target-branches/cli'; -import {CheckoutCommandModule} from './checkout/cli'; -import {buildDiscoverNewConflictsCommand, handleDiscoverNewConflictsCommand} from './discover-new-conflicts/cli'; -import {MergeCommandModule} from './merge/cli'; -import {buildRebaseCommand, handleRebaseCommand} from './rebase/cli'; - -/** Build the parser for pull request commands. */ -export function buildPrParser(localYargs: yargs.Argv) { - return localYargs.help() - .strict() - .demandCommand() - .command( - 'discover-new-conflicts ', - 'Check if a pending PR causes new conflicts for other pending PRs', - buildDiscoverNewConflictsCommand, handleDiscoverNewConflictsCommand) - .command( - 'rebase ', 'Rebase a pending PR and push the rebased commits back to Github', - buildRebaseCommand, handleRebaseCommand) - .command(MergeCommandModule) - .command(CheckoutCommandModule) - .command(CheckTargetBranchesModule); -} diff --git a/dev-infra/pr/common/BUILD.bazel b/dev-infra/pr/common/BUILD.bazel deleted file mode 100644 index 36c43925dffb49..00000000000000 --- a/dev-infra/pr/common/BUILD.bazel +++ /dev/null @@ -1,12 +0,0 @@ -load("//dev-infra:defaults.bzl", "ts_library") - -ts_library( - name = "common", - srcs = glob(["*.ts"]), - visibility = ["//dev-infra:__subpackages__"], - deps = [ - "//dev-infra/utils", - "@npm//@types/node", - "@npm//typed-graphqlify", - ], -) diff --git a/dev-infra/pr/common/checkout-pr.ts b/dev-infra/pr/common/checkout-pr.ts deleted file mode 100644 index 998bbba421ff68..00000000000000 --- a/dev-infra/pr/common/checkout-pr.ts +++ /dev/null @@ -1,128 +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 {types as graphqlTypes} from 'typed-graphqlify'; - -import {info} from '../../utils/console'; -import {AuthenticatedGitClient} from '../../utils/git/authenticated-git-client'; -import {addTokenToGitHttpsUrl} from '../../utils/git/github-urls'; -import {getPr} from '../../utils/github'; - -/* Graphql schema for the response body for a pending PR. */ -const PR_SCHEMA = { - state: graphqlTypes.string, - maintainerCanModify: graphqlTypes.boolean, - viewerDidAuthor: graphqlTypes.boolean, - headRefOid: graphqlTypes.string, - headRef: { - name: graphqlTypes.string, - repository: { - url: graphqlTypes.string, - nameWithOwner: graphqlTypes.string, - }, - }, - baseRef: { - name: graphqlTypes.string, - repository: { - url: graphqlTypes.string, - nameWithOwner: graphqlTypes.string, - }, - }, -}; - - -export class UnexpectedLocalChangesError extends Error { - constructor(m: string) { - super(m); - Object.setPrototypeOf(this, UnexpectedLocalChangesError.prototype); - } -} - -export class MaintainerModifyAccessError extends Error { - constructor(m: string) { - super(m); - Object.setPrototypeOf(this, MaintainerModifyAccessError.prototype); - } -} - -/** Options for checking out a PR */ -export interface PullRequestCheckoutOptions { - /** Whether the PR should be checked out if the maintainer cannot modify. */ - allowIfMaintainerCannotModify?: boolean; -} - -/** - * Rebase the provided PR onto its merge target branch, and push up the resulting - * commit to the PRs repository. - */ -export async function checkOutPullRequestLocally( - prNumber: number, githubToken: string, opts: PullRequestCheckoutOptions = {}) { - /** The singleton instance of the authenticated git client. */ - const git = AuthenticatedGitClient.get(); - - // In order to preserve local changes, checkouts cannot occur if local changes are present in the - // git environment. Checked before retrieving the PR to fail fast. - if (git.hasUncommittedChanges()) { - throw new UnexpectedLocalChangesError('Unable to checkout PR due to uncommitted changes.'); - } - - /** - * The branch or revision originally checked out before this method performed - * any Git operations that may change the working branch. - */ - const previousBranchOrRevision = git.getCurrentBranchOrRevision(); - /* The PR information from Github. */ - const pr = await getPr(PR_SCHEMA, prNumber, git); - /** The branch name of the PR from the repository the PR came from. */ - const headRefName = pr.headRef.name; - /** The full ref for the repository and branch the PR came from. */ - const fullHeadRef = `${pr.headRef.repository.nameWithOwner}:${headRefName}`; - /** The full URL path of the repository the PR came from with github token as authentication. */ - const headRefUrl = addTokenToGitHttpsUrl(pr.headRef.repository.url, githubToken); - // Note: Since we use a detached head for rebasing the PR and therefore do not have - // remote-tracking branches configured, we need to set our expected ref and SHA. This - // allows us to use `--force-with-lease` for the detached head while ensuring that we - // never accidentally override upstream changes that have been pushed in the meanwhile. - // See: - // https://git-scm.com/docs/git-push#Documentation/git-push.txt---force-with-leaseltrefnamegtltexpectgt - /** Flag for a force push with lease back to upstream. */ - const forceWithLeaseFlag = `--force-with-lease=${headRefName}:${pr.headRefOid}`; - - // If the PR does not allow maintainers to modify it, exit as the rebased PR cannot - // be pushed up. - if (!pr.maintainerCanModify && !pr.viewerDidAuthor && !opts.allowIfMaintainerCannotModify) { - throw new MaintainerModifyAccessError('PR is not set to allow maintainers to modify the PR'); - } - - try { - // Fetch the branch at the commit of the PR, and check it out in a detached state. - info(`Checking out PR #${prNumber} from ${fullHeadRef}`); - git.run(['fetch', '-q', headRefUrl, headRefName]); - git.run(['checkout', '--detach', 'FETCH_HEAD']); - } catch (e) { - git.checkout(previousBranchOrRevision, true); - throw e; - } - - return { - /** - * Pushes the current local branch to the PR on the upstream repository. - * - * @returns true If the command did not fail causing a GitCommandError to be thrown. - * @throws GitCommandError Thrown when the push back to upstream fails. - */ - pushToUpstream: (): true => { - git.run(['push', headRefUrl, `HEAD:${headRefName}`, forceWithLeaseFlag]); - return true; - }, - /** Restores the state of the local repository to before the PR checkout occured. */ - resetGitState: (): boolean => { - return git.checkout(previousBranchOrRevision, true); - } - }; -} diff --git a/dev-infra/pr/discover-new-conflicts/BUILD.bazel b/dev-infra/pr/discover-new-conflicts/BUILD.bazel deleted file mode 100644 index e88ed7eaeda4cd..00000000000000 --- a/dev-infra/pr/discover-new-conflicts/BUILD.bazel +++ /dev/null @@ -1,17 +0,0 @@ -load("//dev-infra:defaults.bzl", "ts_library") - -ts_library( - name = "discover-new-conflicts", - srcs = [ - "cli.ts", - "index.ts", - ], - visibility = ["//dev-infra:__subpackages__"], - deps = [ - "//dev-infra/utils", - "@npm//@types/cli-progress", - "@npm//@types/node", - "@npm//@types/yargs", - "@npm//typed-graphqlify", - ], -) diff --git a/dev-infra/pr/discover-new-conflicts/cli.ts b/dev-infra/pr/discover-new-conflicts/cli.ts deleted file mode 100644 index 11e4cd0f248d4e..00000000000000 --- a/dev-infra/pr/discover-new-conflicts/cli.ts +++ /dev/null @@ -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 {Arguments, Argv} from 'yargs'; - -import {error} from '../../utils/console'; -import {addGithubTokenOption} from '../../utils/git/github-yargs'; - -import {discoverNewConflictsForPr} from './index'; - -/** The options available to the discover-new-conflicts command via CLI. */ -export interface DiscoverNewConflictsCommandOptions { - date: number; - 'pr-number': number; -} - -/** Builds the discover-new-conflicts pull request command. */ -export function buildDiscoverNewConflictsCommand(yargs: Argv): - Argv { - return addGithubTokenOption(yargs) - .option('date', { - description: 'Only consider PRs updated since provided date', - defaultDescription: '30 days ago', - coerce: (date) => typeof date === 'number' ? date : Date.parse(date), - default: getThirtyDaysAgoDate(), - }) - .positional('pr-number', {demandOption: true, type: 'number'}); -} - -/** Handles the discover-new-conflicts pull request command. */ -export async function handleDiscoverNewConflictsCommand( - {'pr-number': prNumber, date}: Arguments) { - // If a provided date is not able to be parsed, yargs provides it as NaN. - if (isNaN(date)) { - error('Unable to parse the value provided via --date flag'); - process.exit(1); - } - await discoverNewConflictsForPr(prNumber, date); -} - -/** Gets a date object 30 days ago from today. */ -function getThirtyDaysAgoDate() { - const date = new Date(); - // Set the hours, minutes and seconds to 0 to only consider date. - date.setHours(0, 0, 0, 0); - // Set the date to 30 days in the past. - date.setDate(date.getDate() - 30); - return date.getTime(); -} diff --git a/dev-infra/pr/discover-new-conflicts/index.ts b/dev-infra/pr/discover-new-conflicts/index.ts deleted file mode 100644 index 7438b02dfb0c29..00000000000000 --- a/dev-infra/pr/discover-new-conflicts/index.ts +++ /dev/null @@ -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 {Bar} from 'cli-progress'; -import {types as graphqlTypes} from 'typed-graphqlify'; - -import {error, info} from '../../utils/console'; -import {AuthenticatedGitClient} from '../../utils/git/authenticated-git-client'; -import {GitCommandError} from '../../utils/git/git-client'; -import {getPendingPrs} from '../../utils/github'; - - -/* Graphql schema for the response body for each pending PR. */ -const PR_SCHEMA = { - headRef: { - name: graphqlTypes.string, - repository: { - url: graphqlTypes.string, - nameWithOwner: graphqlTypes.string, - }, - }, - baseRef: { - name: graphqlTypes.string, - repository: { - url: graphqlTypes.string, - nameWithOwner: graphqlTypes.string, - }, - }, - updatedAt: graphqlTypes.string, - number: graphqlTypes.number, - mergeable: graphqlTypes.string, - title: graphqlTypes.string, -}; - -/* Pull Request response from Github Graphql query */ -type RawPullRequest = typeof PR_SCHEMA; - -/** Convert raw Pull Request response from Github to usable Pull Request object. */ -function processPr(pr: RawPullRequest) { - return {...pr, updatedAt: (new Date(pr.updatedAt)).getTime()}; -} - -/* Pull Request object after processing, derived from the return type of the processPr function. */ -type PullRequest = ReturnType; - -/** Name of a temporary local branch that is used for checking conflicts. **/ -const tempWorkingBranch = '__NgDevRepoBaseAfterChange__'; - -/** Checks if the provided PR will cause new conflicts in other pending PRs. */ -export async function discoverNewConflictsForPr(newPrNumber: number, updatedAfter: number) { - /** The singleton instance of the authenticated git client. */ - const git = AuthenticatedGitClient.get(); - // If there are any local changes in the current repository state, the - // check cannot run as it needs to move between branches. - if (git.hasUncommittedChanges()) { - error('Cannot run with local changes. Please make sure there are no local changes.'); - process.exit(1); - } - - /** The active github branch or revision before we performed any Git commands. */ - const previousBranchOrRevision = git.getCurrentBranchOrRevision(); - /* Progress bar to indicate progress. */ - const progressBar = new Bar({format: `[{bar}] ETA: {eta}s | {value}/{total}`}); - /* PRs which were found to be conflicting. */ - const conflicts: Array = []; - - info(`Requesting pending PRs from Github`); - /** List of PRs from github currently known as mergeable. */ - const allPendingPRs = (await getPendingPrs(PR_SCHEMA, git)).map(processPr); - /** The PR which is being checked against. */ - const requestedPr = allPendingPRs.find(pr => pr.number === newPrNumber); - if (requestedPr === undefined) { - error( - `The request PR, #${newPrNumber} was not found as a pending PR on github, please confirm`); - error(`the PR number is correct and is an open PR`); - process.exit(1); - } - - const pendingPrs = allPendingPRs.filter(pr => { - return ( - // PRs being merged into the same target branch as the requested PR - pr.baseRef.name === requestedPr.baseRef.name && - // PRs which either have not been processed or are determined as mergeable by Github - pr.mergeable !== 'CONFLICTING' && - // PRs updated after the provided date - pr.updatedAt >= updatedAfter); - }); - info(`Retrieved ${allPendingPRs.length} total pending PRs`); - info(`Checking ${pendingPrs.length} PRs for conflicts after a merge of #${newPrNumber}`); - - // Fetch and checkout the PR being checked. - git.run(['fetch', '-q', requestedPr.headRef.repository.url, requestedPr.headRef.name]); - git.run(['checkout', '-q', '-B', tempWorkingBranch, 'FETCH_HEAD']); - - // Rebase the PR against the PRs target branch. - git.run(['fetch', '-q', requestedPr.baseRef.repository.url, requestedPr.baseRef.name]); - try { - git.run(['rebase', 'FETCH_HEAD'], {stdio: 'ignore'}); - } catch (err) { - if (err instanceof GitCommandError) { - error('The requested PR currently has conflicts'); - git.checkout(previousBranchOrRevision, true); - process.exit(1); - } - throw err; - } - - // Start the progress bar - progressBar.start(pendingPrs.length, 0); - - // Check each PR to determine if it can merge cleanly into the repo after the target PR. - for (const pr of pendingPrs) { - // Fetch and checkout the next PR - git.run(['fetch', '-q', pr.headRef.repository.url, pr.headRef.name]); - git.run(['checkout', '-q', '--detach', 'FETCH_HEAD']); - // Check if the PR cleanly rebases into the repo after the target PR. - try { - git.run(['rebase', tempWorkingBranch], {stdio: 'ignore'}); - } catch (err) { - if (err instanceof GitCommandError) { - conflicts.push(pr); - } else { - throw err; - } - } - // Abort any outstanding rebase attempt. - git.runGraceful(['rebase', '--abort'], {stdio: 'ignore'}); - - progressBar.increment(1); - } - // End the progress bar as all PRs have been processed. - progressBar.stop(); - info(); - info(`Result:`); - - git.checkout(previousBranchOrRevision, true); - - // If no conflicts are found, exit successfully. - if (conflicts.length === 0) { - info(`No new conflicting PRs found after #${newPrNumber} merging`); - process.exit(0); - } - - // Inform about discovered conflicts, exit with failure. - error.group(`${conflicts.length} PR(s) which conflict(s) after #${newPrNumber} merges:`); - for (const pr of conflicts) { - error(` - #${pr.number}: ${pr.title}`); - } - error.groupEnd(); - process.exit(1); -} diff --git a/dev-infra/pr/merge/BUILD.bazel b/dev-infra/pr/merge/BUILD.bazel deleted file mode 100644 index e1d94a8c6a452e..00000000000000 --- a/dev-infra/pr/merge/BUILD.bazel +++ /dev/null @@ -1,60 +0,0 @@ -load("//dev-infra:defaults.bzl", "jasmine_node_test", "ts_library") - -ts_library( - name = "merge", - srcs = glob( - ["**/*.ts"], - exclude = ["**/*.spec.ts"], - ), - - # prodmode target must be set to es5 as the Ora class, which is extended is not a proper - # prototyped object. - prodmode_target = "es5", - visibility = ["//dev-infra:__subpackages__"], - deps = [ - "//dev-infra/commit-message", - "//dev-infra/release/config", - "//dev-infra/release/versioning", - "//dev-infra/utils", - "@npm//@octokit/plugin-rest-endpoint-methods", - "@npm//@octokit/rest", - "@npm//@types/inquirer", - "@npm//@types/node", - "@npm//@types/node-fetch", - "@npm//@types/semver", - "@npm//@types/yargs", - "@npm//chalk", - "@npm//typed-graphqlify", - ], -) - -ts_library( - name = "test_lib", - testonly = True, - srcs = glob(["**/*.spec.ts"]), - deps = [ - ":merge", - "//dev-infra/release/config", - "//dev-infra/release/versioning", - "//dev-infra/utils", - "//dev-infra/utils/testing", - "@npm//@types/jasmine", - "@npm//@types/node", - "@npm//@types/node-fetch", - "@npm//nock", - ], -) - -jasmine_node_test( - name = "test", - # Disable the Bazel patched module resolution. It always loads ".mjs" files first. This - # breaks NodeJS execution for "node-fetch" as it uses experimental modules which are not - # enabled in NodeJS. TODO: Remove this with rules_nodejs 3.x where patching is optional. - # https://github.com/bazelbuild/rules_nodejs/commit/7d070ffadf9c3b41711382a4737b995f987c14fa. - args = ["--nobazel_patch_module_resolver"], - deps = [ - ":test_lib", - "@npm//node-fetch", - "@npm//semver", - ], -) diff --git a/dev-infra/pr/merge/cli.ts b/dev-infra/pr/merge/cli.ts deleted file mode 100644 index e752422d74f073..00000000000000 --- a/dev-infra/pr/merge/cli.ts +++ /dev/null @@ -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 - */ - -import {Arguments, Argv, CommandModule} from 'yargs'; - -import {addGithubTokenOption} from '../../utils/git/github-yargs'; - -import {mergePullRequest} from './index'; - -/** The options available to the merge command via CLI. */ -export interface MergeCommandOptions { - githubToken: string; - pr: number; - branchPrompt: boolean; -} - -/** Builds the command. */ -function builder(yargs: Argv) { - return addGithubTokenOption(yargs) - .help() - .strict() - .positional('pr', { - demandOption: true, - type: 'number', - description: 'The PR to be merged.', - }) - .option('branch-prompt' as 'branchPrompt', { - type: 'boolean', - default: true, - description: 'Whether to prompt to confirm the branches a PR will merge into.', - }); -} - -/** Handles the command. */ -async function handler({pr, branchPrompt}: Arguments) { - await mergePullRequest(pr, {branchPrompt}); -} - -/** yargs command module describing the command. */ -export const MergeCommandModule: CommandModule<{}, MergeCommandOptions> = { - handler, - builder, - command: 'merge ', - describe: 'Merge a PR into its targeted branches.', -}; diff --git a/dev-infra/pr/merge/config.ts b/dev-infra/pr/merge/config.ts deleted file mode 100644 index b34df2ed24b53a..00000000000000 --- a/dev-infra/pr/merge/config.ts +++ /dev/null @@ -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 {GitClientConfig, NgDevConfig} from '../../utils/config'; -import {GithubClient} from '../../utils/git/github'; - -import {GithubApiMergeStrategyConfig} from './strategies/api-merge'; - -/** Describes possible values that can be returned for `branches` of a target label. */ -export type TargetLabelBranchResult = string[]|Promise; - -/** - * Possible merge methods supported by the Github API. - * https://developer.github.com/v3/pulls/#merge-a-pull-request-merge-button. - */ -export type GithubApiMergeMethod = 'merge'|'squash'|'rebase'; - -/** - * Target labels represent Github pull requests labels. These labels instruct the merge - * script into which branches a given pull request should be merged to. - */ -export interface TargetLabel { - /** Pattern that matches the given target label. */ - pattern: RegExp|string; - /** - * List of branches a pull request with this target label should be merged into. - * Can also be wrapped in a function that accepts the target branch specified in the - * Github Web UI. This is useful for supporting labels like `target: development-branch`. - * - * @throws {InvalidTargetLabelError} Invalid label has been applied to pull request. - * @throws {InvalidTargetBranchError} Invalid Github target branch has been selected. - */ - branches: TargetLabelBranchResult|((githubTargetBranch: string) => TargetLabelBranchResult); -} - -/** - * Configuration for the merge script with all remote options specified. The - * default `MergeConfig` has does not require any of these options as defaults - * are provided by the common dev-infra github configuration. - */ -export type MergeConfigWithRemote = MergeConfig&{remote: GitClientConfig}; - -/** Configuration for the merge script. */ -export interface MergeConfig { - /** - * Configuration for the upstream remote. All of these options are optional as - * defaults are provided by the common dev-infra github configuration. - */ - remote?: GitClientConfig; - /** List of target labels. */ - labels: TargetLabel[]; - /** Required base commits for given branches. */ - requiredBaseCommits?: {[branchName: string]: string}; - /** Pattern that matches labels which imply a signed CLA. */ - claSignedLabel: string|RegExp; - /** Pattern that matches labels which imply a merge ready pull request. */ - mergeReadyLabel: string|RegExp; - /** Label that is applied when special attention from the caretaker is required. */ - caretakerNoteLabel?: string|RegExp; - /** Label which can be applied to fixup commit messages in the merge script. */ - commitMessageFixupLabel: string|RegExp; - /** Label that is applied when a breaking change is made in the pull request. */ - breakingChangeLabel?: string; - /** - * Whether pull requests should be merged using the Github API. This can be enabled - * if projects want to have their pull requests show up as `Merged` in the Github UI. - * The downside is that fixup or squash commits no longer work as the Github API does - * not support this. - */ - githubApiMerge: false|GithubApiMergeStrategyConfig; - /** - * List of commit scopes which are exempted from target label content requirements. i.e. no `feat` - * scopes in patch branches, no breaking changes in minor or patch changes. - */ - targetLabelExemptScopes?: string[]; -} - -/** - * Configuration of the merge script in the dev-infra configuration. Note that the - * merge configuration is retrieved lazily as usually these configurations rely - * on branch name computations. We don't want to run these immediately whenever - * the dev-infra configuration is loaded as that could slow-down other commands. - */ -export type DevInfraMergeConfig = - NgDevConfig<{'merge': (api: GithubClient) => MergeConfig | Promise}>; - -/** Loads and validates the merge configuration. */ -export async function loadAndValidateConfig( - config: Partial, - api: GithubClient): Promise<{config?: MergeConfig, errors?: string[]}> { - if (config.merge === undefined) { - return {errors: ['No merge configuration found. Set the `merge` configuration.']}; - } - - if (typeof config.merge !== 'function') { - return {errors: ['Expected merge configuration to be defined lazily through a function.']}; - } - - const mergeConfig = await config.merge(api); - const errors = validateMergeConfig(mergeConfig); - - if (errors.length) { - return {errors}; - } - - return {config: mergeConfig}; -} - -/** Validates the specified configuration. Returns a list of failure messages. */ -function validateMergeConfig(config: Partial): string[] { - const errors: string[] = []; - if (!config.labels) { - errors.push('No label configuration.'); - } else if (!Array.isArray(config.labels)) { - errors.push('Label configuration needs to be an array.'); - } - if (!config.claSignedLabel) { - errors.push('No CLA signed label configured.'); - } - if (!config.mergeReadyLabel) { - errors.push('No merge ready label configured.'); - } - if (config.githubApiMerge === undefined) { - errors.push('No explicit choice of merge strategy. Please set `githubApiMerge`.'); - } - return errors; -} diff --git a/dev-infra/pr/merge/defaults/index.ts b/dev-infra/pr/merge/defaults/index.ts deleted file mode 100644 index 292bf3fb3ebd79..00000000000000 --- a/dev-infra/pr/merge/defaults/index.ts +++ /dev/null @@ -1,10 +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 * from './labels'; -export * from './lts-branch'; diff --git a/dev-infra/pr/merge/defaults/integration.spec.ts b/dev-infra/pr/merge/defaults/integration.spec.ts deleted file mode 100644 index 83a86bb2a8862c..00000000000000 --- a/dev-infra/pr/merge/defaults/integration.spec.ts +++ /dev/null @@ -1,508 +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 nock from 'nock'; -import {ParsedUrlQuery} from 'querystring'; - -import {ReleaseConfig} from '../../../release/config/index'; -import {_npmPackageInfoCache, NpmPackageInfo} from '../../../release/versioning/npm-registry'; -import {GithubConfig} from '../../../utils/config'; -import * as console from '../../../utils/console'; -import {GithubClient} from '../../../utils/git/github'; -import {buildGithubPaginationResponseHeader} from '../../../utils/testing/github-pagination-header'; -import {TargetLabel} from '../config'; -import {getBranchesFromTargetLabel, getTargetLabelFromPullRequest} from '../target-label'; - -import {getDefaultTargetLabelConfiguration} from './index'; - -const API_ENDPOINT = `https://api.github.com`; - -describe('default target labels', () => { - let api: GithubClient; - let githubConfig: GithubConfig; - let releaseConfig: ReleaseConfig; - - beforeEach(() => { - api = new GithubClient(); - githubConfig = {owner: 'angular', name: 'dev-infra-test'}; - releaseConfig = { - npmPackages: ['@angular/dev-infra-test-pkg'], - buildPackages: async () => [], - releaseNotes: {} - }; - - // The label determination will print warn messages. These should not be - // printed to the console, so we turn `console.warn` into a spy. - spyOn(console, 'warn'); - }); - - afterEach(() => nock.cleanAll()); - - async function computeTargetLabels(): Promise { - return getDefaultTargetLabelConfiguration(api, githubConfig, releaseConfig); - } - - function getRepoApiRequestUrl(): string { - return `${API_ENDPOINT}/repos/${githubConfig.owner}/${githubConfig.name}`; - } - - /** - * Mocks a branch `package.json` version API request. - * https://docs.github.com/en/rest/reference/repos#get-repository-content. - */ - function interceptBranchVersionRequest(branchName: string, version: string) { - nock(getRepoApiRequestUrl()) - .get('/contents/%2Fpackage.json') - .query(params => params.ref === branchName) - .reply(200, {content: Buffer.from(JSON.stringify({version})).toString('base64')}); - } - - /** Fakes a prompt confirm question with the given value. */ - function fakePromptConfirmValue(returnValue: boolean) { - spyOn(console, 'promptConfirm').and.resolveTo(returnValue); - } - - /** Fakes a NPM package query API request. */ - function fakeNpmPackageQueryRequest(data: Partial) { - _npmPackageInfoCache[releaseConfig.npmPackages[0]] = - Promise.resolve({'dist-tags': {}, versions: {}, time: {}, ...data}); - } - - /** - * Mocks a repository branch list API request. - * https://docs.github.com/en/rest/reference/repos#list-branches. - */ - function interceptBranchesListRequest(branches: string[]) { - nock(getRepoApiRequestUrl()) - .get('/branches') - .query(true) - .reply(200, branches.slice(0, 29).map(name => ({name}))); - } - - /** - * Mocks a repository branch list API request with pagination. - * https://docs.github.com/en/rest/guides/traversing-with-pagination. - * https://docs.github.com/en/rest/reference/repos#list-branches. - */ - function interceptBranchesListRequestWithPagination(branches: string[]) { - const apiUrl = getRepoApiRequestUrl(); - - // For each branch, create its own API page so that pagination is required - // to resolve all given branches. - for (let index = 0; index < branches.length; index++) { - // Pages start with `1` as per the Github API specification. - const pageNum = index + 1; - const name = branches[index]; - const linkHeader = - buildGithubPaginationResponseHeader(branches.length, pageNum, `${apiUrl}/branches`); - - // For the first page, either `?page=1` needs to be set, or no `page` should be specified. - const queryMatch = pageNum === 1 ? - (params: ParsedUrlQuery) => params.page === '1' || params.page === undefined : - {page: pageNum}; - - nock(getRepoApiRequestUrl()).get('/branches').query(queryMatch).reply(200, [{name}], { - link: linkHeader, - }); - } - } - - async function getBranchesForLabel( - name: string, githubTargetBranch = 'master', labels?: TargetLabel[]): Promise { - if (labels === undefined) { - labels = await computeTargetLabels(); - } - let label: TargetLabel; - try { - label = getTargetLabelFromPullRequest({labels}, [name]); - } catch (error) { - return null; - } - return await getBranchesFromTargetLabel(label, githubTargetBranch); - } - - it('should detect "master" as branch for target: minor', async () => { - interceptBranchVersionRequest('master', '11.0.0-next.0'); - interceptBranchVersionRequest('10.2.x', '10.2.4'); - - // Note: We add a few more branches here to ensure that branches API requests are - // paginated properly. In Angular projects, there are usually many branches so that - // pagination is ultimately needed to detect the active release trains. - // See: https://github.com/angular/angular/commit/261b060fa168754db00248d1c5c9574bb19a72b4. - interceptBranchesListRequestWithPagination(['9.8.x', '10.1.x', '10.2.x']); - - expect(await getBranchesForLabel('target: minor')).toEqual(['master']); - }); - - it('should error if non version-branch is targeted with "target: lts"', async () => { - interceptBranchVersionRequest('master', '11.0.0-next.0'); - interceptBranchVersionRequest('10.2.x', '10.2.4'); - interceptBranchesListRequest(['10.2.x']); - - await expectAsync(getBranchesForLabel('target: lts', 'master')) - .toBeRejectedWith(jasmine.objectContaining({ - failureMessage: - 'PR cannot be merged as it does not target a long-term support branch: "master"' - })); - }); - - it('should error if patch branch is targeted with "target: lts"', async () => { - interceptBranchVersionRequest('master', '11.0.0-next.0'); - interceptBranchVersionRequest('10.2.x', '10.2.4'); - interceptBranchesListRequest(['10.2.x']); - - await expectAsync(getBranchesForLabel('target: lts', '10.2.x')) - .toBeRejectedWith(jasmine.objectContaining({ - failureMessage: - 'PR cannot be merged with "target: lts" into patch branch. Consider changing the ' + - 'label to "target: patch" if this is intentional.' - })); - }); - - it('should error if feature-freeze branch is targeted with "target: lts"', async () => { - interceptBranchVersionRequest('master', '11.0.0-next.0'); - interceptBranchVersionRequest('10.2.x', '10.2.0-next.0'); - interceptBranchVersionRequest('10.1.x', '10.1.0'); - interceptBranchesListRequest(['10.1.x', '10.2.x']); - - await expectAsync(getBranchesForLabel('target: lts', '10.2.x')) - .toBeRejectedWith(jasmine.objectContaining({ - failureMessage: - 'PR cannot be merged with "target: lts" into feature-freeze/release-candidate branch. ' + - 'Consider changing the label to "target: rc" if this is intentional.' - })); - }); - - it('should error if release-candidate branch is targeted with "target: lts"', async () => { - interceptBranchVersionRequest('master', '11.0.0-next.0'); - interceptBranchVersionRequest('10.2.x', '10.2.0-rc.0'); - interceptBranchVersionRequest('10.1.x', '10.1.0'); - interceptBranchesListRequest(['10.1.x', '10.2.x']); - - await expectAsync(getBranchesForLabel('target: lts', '10.2.x')) - .toBeRejectedWith(jasmine.objectContaining({ - failureMessage: - 'PR cannot be merged with "target: lts" into feature-freeze/release-candidate branch. ' + - 'Consider changing the label to "target: rc" if this is intentional.' - })); - }); - - it('should error if branch targeted with "target: lts" is no longer active', async () => { - interceptBranchVersionRequest('master', '11.1.0-next.0'); - interceptBranchVersionRequest('11.0.x', '11.0.0'); - interceptBranchVersionRequest('10.5.x', '10.5.1'); - interceptBranchesListRequest(['10.5.x', '11.0.x']); - - // We support forcibly proceeding with merging if a given branch previously was in LTS mode - // but no longer is (after a period of time). In this test, we are not forcibly proceeding. - fakePromptConfirmValue(false); - fakeNpmPackageQueryRequest({ - 'dist-tags': { - 'v10-lts': '10.5.1', - }, - 'time': { - // v10 has been released at the given specified date. We pick a date that - // guarantees that the version is no longer considered as active LTS version. - '10.0.0': new Date(1912, 5, 23).toISOString(), - } - }); - - await expectAsync(getBranchesForLabel('target: lts', '10.5.x')) - .toBeRejectedWith(jasmine.objectContaining({ - failureMessage: - 'Long-term supported ended for v10 on 12/23/1913. Pull request cannot be merged ' + - 'into the 10.5.x branch.' - })); - }); - - it('should error if branch targeted with "target: lts" is not latest LTS for given major', - async () => { - interceptBranchVersionRequest('master', '11.1.0-next.0'); - interceptBranchVersionRequest('11.0.x', '11.0.0'); - interceptBranchVersionRequest('10.5.x', '10.5.1'); - interceptBranchVersionRequest('10.4.x', '10.4.4'); - interceptBranchesListRequest(['10.4.x', '10.5.x', '11.0.x']); - - fakeNpmPackageQueryRequest({ - 'dist-tags': { - 'v10-lts': '10.5.1', - } - }); - - await expectAsync(getBranchesForLabel('target: lts', '10.4.x')) - .toBeRejectedWith(jasmine.objectContaining({ - failureMessage: - 'Not using last-minor branch for v10 LTS version. PR should be updated to ' + - 'target: 10.5.x' - })); - }); - - it('should error if branch targeted with "target: lts" is not a major version with LTS', - async () => { - interceptBranchVersionRequest('master', '11.1.0-next.0'); - interceptBranchVersionRequest('11.0.x', '11.0.0'); - interceptBranchVersionRequest('10.5.x', '10.5.1'); - interceptBranchesListRequest(['10.5.x', '11.0.x']); - - fakeNpmPackageQueryRequest({'dist-tags': {}}); - - await expectAsync(getBranchesForLabel('target: lts', '10.5.x')) - .toBeRejectedWith( - jasmine.objectContaining({failureMessage: 'No LTS version tagged for v10 in NPM.'})); - }); - - it('should allow forcibly proceeding with merge if branch targeted with "target: lts" is no ' + - 'longer active', - async () => { - interceptBranchVersionRequest('master', '11.1.0-next.0'); - interceptBranchVersionRequest('11.0.x', '11.0.0'); - interceptBranchVersionRequest('10.5.x', '10.5.1'); - interceptBranchesListRequest(['10.5.x', '11.0.x']); - - // We support forcibly proceeding with merging if a given branch previously was in LTS mode - // but no longer is (after a period of time). In this test, we are forcibly proceeding and - // expect the Github target branch to be picked up as branch for the `target: lts` label. - fakePromptConfirmValue(true); - fakeNpmPackageQueryRequest({ - 'dist-tags': { - 'v10-lts': '10.5.1', - }, - 'time': { - // v10 has been released at the given specified date. We pick a date that - // guarantees that the version is no longer considered as active LTS version. - '10.0.0': new Date(1912, 5, 23).toISOString(), - } - }); - - expect(await getBranchesForLabel('target: lts', '10.5.x')).toEqual(['10.5.x']); - }); - - it('should use target branch for "target: lts" if it matches an active LTS branch', async () => { - interceptBranchVersionRequest('master', '11.1.0-next.0'); - interceptBranchVersionRequest('11.0.x', '11.0.0'); - interceptBranchVersionRequest('10.5.x', '10.5.1'); - interceptBranchesListRequest(['10.5.x', '11.0.x']); - - fakeNpmPackageQueryRequest({ - 'dist-tags': { - 'v10-lts': '10.5.1', - }, - 'time': { - '10.0.0': new Date().toISOString(), - } - }); - - expect(await getBranchesForLabel('target: lts', '10.5.x')).toEqual(['10.5.x']); - }); - - it('should error if no active branch for given major version could be found', async () => { - interceptBranchVersionRequest('master', '12.0.0-next.0'); - interceptBranchesListRequest(['9.0.x', '9.1.x']); - - await expectAsync(getBranchesForLabel('target: lts', '10.2.x')) - .toBeRejectedWithError( - 'Unable to determine the latest release-train. The following branches have ' + - 'been considered: []'); - }); - - it('should error if invalid version is set for version-branch', async () => { - interceptBranchVersionRequest('master', '11.2.0-next.0'); - interceptBranchVersionRequest('11.1.x', '11.1.x'); - interceptBranchesListRequest(['11.1.x']); - - await expectAsync(getBranchesForLabel('target: lts', '10.2.x')) - .toBeRejectedWithError('Invalid version detected in following branch: 11.1.x.'); - }); - - it('should error if version-branch more recent than "next" is discovered', async () => { - interceptBranchVersionRequest('master', '11.2.0-next.0'); - interceptBranchVersionRequest('11.3.x', '11.3.0-next.0'); - interceptBranchVersionRequest('11.1.x', '11.1.5'); - interceptBranchesListRequest(['11.1.x', '11.3.x']); - - await expectAsync(getBranchesForLabel('target: lts', '10.2.x')) - .toBeRejectedWithError( - 'Discovered unexpected version-branch "11.3.x" for a release-train that is ' + - 'more recent than the release-train currently in the "master" branch. Please ' + - 'either delete the branch if created by accident, or update the outdated version ' + - 'in the next branch (master).'); - }); - - it('should error if branch is matching with release-train in the "next" branch', async () => { - interceptBranchVersionRequest('master', '11.2.0-next.0'); - interceptBranchVersionRequest('11.2.x', '11.2.0-next.0'); - interceptBranchVersionRequest('11.1.x', '11.1.5'); - interceptBranchesListRequest(['11.1.x', '11.2.x']); - - await expectAsync(getBranchesForLabel('target: lts', '10.2.x')) - .toBeRejectedWithError( - 'Discovered unexpected version-branch "11.2.x" for a release-train that is already ' + - 'active in the "master" branch. Please either delete the branch if created by ' + - 'accident, or update the version in the next branch (master).'); - }); - - it('should allow merging PR only into patch branch with "target: patch"', async () => { - interceptBranchVersionRequest('master', '11.2.0-next.0'); - interceptBranchVersionRequest('11.1.x', '11.1.0'); - interceptBranchesListRequest(['11.1.x']); - - expect(await getBranchesForLabel('target: patch', '11.1.x')).toEqual(['11.1.x']); - }); - - describe('next: major release', () => { - it('should detect "master" as branch for target: major', async () => { - interceptBranchVersionRequest('master', '11.0.0-next.0'); - interceptBranchVersionRequest('10.2.x', '10.2.4'); - interceptBranchesListRequest(['10.2.x']); - - expect(await getBranchesForLabel('target: major')).toEqual(['master']); - }); - - describe('without active release-candidate', () => { - it('should detect last-minor from previous major as branch for target: patch', async () => { - interceptBranchVersionRequest('master', '11.0.0-next.0'); - interceptBranchVersionRequest('10.2.x', '10.2.4'); - interceptBranchesListRequest(['10.0.x', '10.1.x', '10.2.x']); - - expect(await getBranchesForLabel('target: patch')).toEqual(['master', '10.2.x']); - }); - - it('should error if "target: rc" is applied', async () => { - interceptBranchVersionRequest('master', '11.0.0-next.0'); - interceptBranchVersionRequest('10.2.x', '10.2.4'); - interceptBranchesListRequest(['10.0.x', '10.1.x', '10.2.x']); - - await expectAsync(getBranchesForLabel('target: rc')) - .toBeRejectedWith(jasmine.objectContaining({ - failureMessage: - 'No active feature-freeze/release-candidate branch. Unable to merge ' + - 'pull request using "target: rc" label.' - })); - }); - }); - - describe('with active release-candidate', () => { - it('should detect most recent non-prerelease minor branch from previous major for ' + - 'target: patch', - async () => { - interceptBranchVersionRequest('master', '11.0.0-next.0'); - interceptBranchVersionRequest('10.2.x', '10.2.0-rc.0'); - interceptBranchVersionRequest('10.1.x', '10.2.3'); - interceptBranchesListRequest(['10.1.x', '10.2.x']); - - // Pull requests should also be merged into the RC and `next` (i.e. `master`) branch. - expect(await getBranchesForLabel('target: patch')).toEqual([ - 'master', '10.1.x', '10.2.x' - ]); - }); - - it('should detect release-candidate branch for "target: rc"', async () => { - interceptBranchVersionRequest('master', '11.0.0-next.0'); - interceptBranchVersionRequest('10.2.x', '10.2.0-rc.0'); - interceptBranchVersionRequest('10.1.x', '10.1.0'); - interceptBranchesListRequest(['10.0.x', '10.1.x', '10.2.x']); - - expect(await getBranchesForLabel('target: rc')).toEqual(['master', '10.2.x']); - }); - - it('should detect feature-freeze branch with "target: rc"', async () => { - interceptBranchVersionRequest('master', '11.0.0-next.0'); - interceptBranchVersionRequest('10.2.x', '10.2.0-next.0'); - interceptBranchVersionRequest('10.1.x', '10.1.0'); - interceptBranchesListRequest(['10.0.x', '10.1.x', '10.2.x']); - - expect(await getBranchesForLabel('target: rc')).toEqual(['master', '10.2.x']); - }); - - it('should error if multiple consecutive release-candidate branches are found', async () => { - interceptBranchVersionRequest('master', '11.0.0-next.0'); - interceptBranchVersionRequest('10.4.x', '10.4.0-next.0'); - interceptBranchVersionRequest('10.3.x', '10.4.0-rc.5'); - interceptBranchesListRequest(['10.3.x', '10.4.x']); - - await expectAsync(getBranchesForLabel('target: patch')) - .toBeRejectedWithError( - 'Unable to determine latest release-train. Found two consecutive ' + - 'branches in feature-freeze/release-candidate phase. Did not expect both ' + - '"10.3.x" and "10.4.x" to be in feature-freeze/release-candidate mode.'); - }); - }); - }); - - describe('next: minor release', () => { - it('should error if "target: major" is applied', async () => { - interceptBranchVersionRequest('master', '11.2.0-next.0'); - interceptBranchVersionRequest('11.1.x', '11.1.4'); - interceptBranchesListRequest(['11.1.x']); - - await expectAsync(getBranchesForLabel('target: major')) - .toBeRejectedWith(jasmine.objectContaining({ - failureMessage: - 'Unable to merge pull request. The "master" branch will be released as ' + - 'a minor version.', - })); - }); - - describe('without active release-candidate', () => { - it('should detect last-minor from previous major as branch for target: patch', async () => { - interceptBranchVersionRequest('master', '11.2.0-next.0'); - interceptBranchVersionRequest('11.1.x', '11.1.0'); - interceptBranchesListRequest(['11.1.x']); - - expect(await getBranchesForLabel('target: patch')).toEqual(['master', '11.1.x']); - }); - - it('should error if "target: rc" is applied', async () => { - interceptBranchVersionRequest('master', '11.2.0-next.0'); - interceptBranchVersionRequest('11.1.x', '11.1.0'); - interceptBranchesListRequest(['11.1.x']); - - await expectAsync(getBranchesForLabel('target: rc')) - .toBeRejectedWith(jasmine.objectContaining({ - failureMessage: - 'No active feature-freeze/release-candidate branch. Unable to merge pull ' + - 'request using "target: rc" label.' - })); - }); - }); - - describe('with active release-candidate', () => { - it('should detect most recent non-prerelease minor branch from previous major for ' + - 'target: patch', - async () => { - interceptBranchVersionRequest('master', '11.2.0-next.0'); - interceptBranchVersionRequest('11.1.x', '11.1.0-rc.0'); - interceptBranchVersionRequest('11.0.x', '11.0.0'); - interceptBranchesListRequest(['11.0.x', '11.1.x']); - - // Pull requests should also be merged into the RC and `next` (i.e. `master`) branch. - expect(await getBranchesForLabel('target: patch')).toEqual([ - 'master', '11.0.x', '11.1.x' - ]); - }); - - it('should detect release-candidate branch for "target: rc"', async () => { - interceptBranchVersionRequest('master', '11.2.0-next.0'); - interceptBranchVersionRequest('11.1.x', '11.1.0-rc.0'); - interceptBranchVersionRequest('11.0.x', '10.0.0'); - interceptBranchesListRequest(['11.0.x', '11.1.x']); - - expect(await getBranchesForLabel('target: rc')).toEqual(['master', '11.1.x']); - }); - - it('should detect feature-freeze branch with "target: rc"', async () => { - interceptBranchVersionRequest('master', '11.2.0-next.0'); - interceptBranchVersionRequest('11.1.x', '11.1.0-next.0'); - interceptBranchVersionRequest('11.0.x', '10.0.0'); - interceptBranchesListRequest(['11.0.x', '11.1.x']); - - expect(await getBranchesForLabel('target: rc')).toEqual(['master', '11.1.x']); - }); - }); - }); -}); diff --git a/dev-infra/pr/merge/defaults/labels.ts b/dev-infra/pr/merge/defaults/labels.ts deleted file mode 100644 index 57f01c166879b9..00000000000000 --- a/dev-infra/pr/merge/defaults/labels.ts +++ /dev/null @@ -1,129 +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 {ReleaseConfig} from '../../../release/config/index'; -import {fetchActiveReleaseTrains, isVersionBranch, nextBranchName} from '../../../release/versioning'; -import {GithubConfig} from '../../../utils/config'; -import {GithubClient} from '../../../utils/git/github'; -import {TargetLabel} from '../config'; -import {InvalidTargetBranchError, InvalidTargetLabelError} from '../target-label'; - -import {assertActiveLtsBranch} from './lts-branch'; - -/** - * Gets a label configuration for the merge tooling that reflects the default Angular - * organization-wide labeling and branching semantics as outlined in the specification. - * - * https://docs.google.com/document/d/197kVillDwx-RZtSVOBtPb4BBIAw0E9RT3q3v6DZkykU - * - * @param api Instance of an authenticated Github client. - * @param githubConfig Configuration for the Github remote. Used as Git remote - * for the release train branches. - * @param releaseConfig Configuration for the release packages. Used to fetch - * NPM version data when LTS version branches are validated. - */ -export async function getDefaultTargetLabelConfiguration( - api: GithubClient, githubConfig: GithubConfig, - releaseConfig: ReleaseConfig): Promise { - const repo = {owner: githubConfig.owner, name: githubConfig.name, api}; - const {latest, releaseCandidate, next} = await fetchActiveReleaseTrains(repo); - - return [ - { - pattern: 'target: major', - branches: () => { - // If `next` is currently not designated to be a major version, we do not - // allow merging of PRs with `target: major`. - if (!next.isMajor) { - throw new InvalidTargetLabelError( - `Unable to merge pull request. The "${nextBranchName}" branch will be released as ` + - 'a minor version.'); - } - return [nextBranchName]; - }, - }, - { - pattern: 'target: minor', - // Changes labeled with `target: minor` are merged most commonly into the next branch - // (i.e. `master`). In rare cases of an exceptional minor version while being already - // on a major release train, this would need to be overridden manually. - // TODO: Consider handling this automatically by checking if the NPM version matches - // the last-minor. If not, then an exceptional minor might be in progress. See: - // https://docs.google.com/document/d/197kVillDwx-RZtSVOBtPb4BBIAw0E9RT3q3v6DZkykU/edit#heading=h.h7o5pjq6yqd0 - branches: [nextBranchName], - }, - { - pattern: 'target: patch', - branches: githubTargetBranch => { - // If a PR is targeting the latest active version-branch through the Github UI, - // and is also labeled with `target: patch`, then we merge it directly into the - // branch without doing any cherry-picking. This is useful if a PR could not be - // applied cleanly, and a separate PR for the patch branch has been created. - if (githubTargetBranch === latest.branchName) { - return [latest.branchName]; - } - // Otherwise, patch changes are always merged into the next and patch branch. - const branches = [nextBranchName, latest.branchName]; - // Additionally, if there is a release-candidate/feature-freeze release-train - // currently active, also merge the PR into that version-branch. - if (releaseCandidate !== null) { - branches.push(releaseCandidate.branchName); - } - return branches; - } - }, - { - pattern: 'target: rc', - branches: githubTargetBranch => { - // The `target: rc` label cannot be applied if there is no active feature-freeze - // or release-candidate release train. - if (releaseCandidate === null) { - throw new InvalidTargetLabelError( - `No active feature-freeze/release-candidate branch. ` + - `Unable to merge pull request using "target: rc" label.`); - } - // If the PR is targeting the active release-candidate/feature-freeze version branch - // directly through the Github UI and has the `target: rc` label applied, merge it - // only into the release candidate branch. This is useful if a PR did not apply cleanly - // into the release-candidate/feature-freeze branch, and a separate PR has been created. - if (githubTargetBranch === releaseCandidate.branchName) { - return [releaseCandidate.branchName]; - } - // Otherwise, merge into the next and active release-candidate/feature-freeze branch. - return [nextBranchName, releaseCandidate.branchName]; - }, - }, - { - // LTS changes are rare enough that we won't worry about cherry-picking changes into all - // active LTS branches for PRs created against any other branch. Instead, PR authors need - // to manually create separate PRs for desired LTS branches. Additionally, active LT branches - // commonly diverge quickly. This makes cherry-picking not an option for LTS changes. - pattern: 'target: lts', - branches: async githubTargetBranch => { - if (!isVersionBranch(githubTargetBranch)) { - throw new InvalidTargetBranchError( - `PR cannot be merged as it does not target a long-term support ` + - `branch: "${githubTargetBranch}"`); - } - if (githubTargetBranch === latest.branchName) { - throw new InvalidTargetBranchError( - `PR cannot be merged with "target: lts" into patch branch. ` + - `Consider changing the label to "target: patch" if this is intentional.`); - } - if (releaseCandidate !== null && githubTargetBranch === releaseCandidate.branchName) { - throw new InvalidTargetBranchError( - `PR cannot be merged with "target: lts" into feature-freeze/release-candidate ` + - `branch. Consider changing the label to "target: rc" if this is intentional.`); - } - // Assert that the selected branch is an active LTS branch. - await assertActiveLtsBranch(repo, releaseConfig, githubTargetBranch); - return [githubTargetBranch]; - }, - }, - ]; -} diff --git a/dev-infra/pr/merge/defaults/lts-branch.ts b/dev-infra/pr/merge/defaults/lts-branch.ts deleted file mode 100644 index 64e934d72dfc93..00000000000000 --- a/dev-infra/pr/merge/defaults/lts-branch.ts +++ /dev/null @@ -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 semver from 'semver'; - -import {ReleaseConfig} from '../../../release/config/index'; -import {computeLtsEndDateOfMajor, fetchProjectNpmPackageInfo, getLtsNpmDistTagOfMajor, getVersionOfBranch, GithubRepoWithApi} from '../../../release/versioning'; -import {promptConfirm, red, warn, yellow} from '../../../utils/console'; -import {InvalidTargetBranchError} from '../target-label'; - -/** - * Asserts that the given branch corresponds to an active LTS version-branch that can receive - * backport fixes. Throws an error if LTS expired or an invalid branch is selected. - * - * @param repo Repository containing the given branch. Used for Github API queries. - * @param releaseConfig Configuration for releases. Used to query NPM about past publishes. - * @param branchName Branch that is checked to be an active LTS version-branch. - * */ -export async function assertActiveLtsBranch( - repo: GithubRepoWithApi, releaseConfig: ReleaseConfig, branchName: string) { - const version = await getVersionOfBranch(repo, branchName); - const {'dist-tags': distTags, time} = await fetchProjectNpmPackageInfo(releaseConfig); - - // LTS versions should be tagged in NPM in the following format: `v{major}-lts`. - const ltsNpmTag = getLtsNpmDistTagOfMajor(version.major); - const ltsVersion = semver.parse(distTags[ltsNpmTag]); - - // Ensure that there is an LTS version tagged for the given version-branch major. e.g. - // if the version branch is `9.2.x` then we want to make sure that there is an LTS - // version tagged in NPM for `v9`, following the `v{major}-lts` tag convention. - if (ltsVersion === null) { - throw new InvalidTargetBranchError(`No LTS version tagged for v${version.major} in NPM.`); - } - - // Ensure that the correct branch is used for the LTS version. We do not want to merge - // changes to older minor version branches that do not reflect the current LTS version. - if (branchName !== `${ltsVersion.major}.${ltsVersion.minor}.x`) { - throw new InvalidTargetBranchError( - `Not using last-minor branch for v${version.major} LTS version. PR ` + - `should be updated to target: ${ltsVersion.major}.${ltsVersion.minor}.x`); - } - - const today = new Date(); - const majorReleaseDate = new Date(time[`${version.major}.0.0`]); - const ltsEndDate = computeLtsEndDateOfMajor(majorReleaseDate); - - // Check if LTS has already expired for the targeted major version. If so, we do not - // allow the merge as per our LTS guarantees. Can be forcibly overridden if desired. - // See: https://angular.io/guide/releases#support-policy-and-schedule. - if (today > ltsEndDate) { - const ltsEndDateText = ltsEndDate.toLocaleDateString('en-US'); - warn(red(`Long-term support ended for v${version.major} on ${ltsEndDateText}.`)); - warn(yellow( - `Merging of pull requests for this major is generally not ` + - `desired, but can be forcibly ignored.`)); - if (await promptConfirm('Do you want to forcibly proceed with merging?')) { - return; - } - throw new InvalidTargetBranchError( - `Long-term supported ended for v${version.major} on ${ltsEndDateText}. ` + - `Pull request cannot be merged into the ${branchName} branch.`); - } -} diff --git a/dev-infra/pr/merge/failures.ts b/dev-infra/pr/merge/failures.ts deleted file mode 100644 index 29e5725c097859..00000000000000 --- a/dev-infra/pr/merge/failures.ts +++ /dev/null @@ -1,121 +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 {TargetLabel} from './config'; - -/** - * Class that can be used to describe pull request failures. A failure - * is described through a human-readable message and a flag indicating - * whether it is non-fatal or not. - */ -export class PullRequestFailure { - constructor( - /** Human-readable message for the failure */ - public message: string, - /** Whether the failure is non-fatal and can be forcibly ignored. */ - public nonFatal = false) {} - - static claUnsigned() { - return new this(`CLA has not been signed. Please make sure the PR author has signed the CLA.`); - } - - static failingCiJobs() { - return new this(`Failing CI jobs.`, true); - } - - static pendingCiJobs() { - return new this(`Pending CI jobs.`, true); - } - - static notMergeReady() { - return new this(`Not marked as merge ready.`); - } - - static isDraft() { - return new this('Pull request is still in draft.'); - } - - static isClosed() { - return new this('Pull request is already closed.'); - } - - static isMerged() { - return new this('Pull request is already merged.'); - } - - static mismatchingTargetBranch(allowedBranches: string[]) { - return new this( - `Pull request is set to wrong base branch. Please update the PR in the Github UI ` + - `to one of the following branches: ${allowedBranches.join(', ')}.`); - } - - static unsatisfiedBaseSha() { - return new this( - `Pull request has not been rebased recently and could be bypassing CI checks. ` + - `Please rebase the PR.`); - } - - static mergeConflicts(failedBranches: string[]) { - return new this( - `Could not merge pull request into the following branches due to merge ` + - `conflicts: ${ - failedBranches.join(', ')}. Please rebase the PR or update the target label.`); - } - - static unknownMergeError() { - return new this(`Unknown merge error occurred. Please see console output above for debugging.`); - } - - static unableToFixupCommitMessageSquashOnly() { - return new this( - `Unable to fixup commit message of pull request. Commit message can only be ` + - `modified if the PR is merged using squash.`); - } - - static notFound() { - return new this(`Pull request could not be found upstream.`); - } - - static insufficientPermissionsToMerge( - message = `Insufficient Github API permissions to merge pull request. Please ensure that ` + - `your auth token has write access.`) { - return new this(message); - } - - static hasBreakingChanges(label: TargetLabel) { - const message = `Cannot merge into branch for "${label.pattern}" as the pull request has ` + - `breaking changes. Breaking changes can only be merged with the "target: major" label.`; - return new this(message); - } - - static hasDeprecations(label: TargetLabel) { - const message = `Cannot merge into branch for "${label.pattern}" as the pull request ` + - `contains deprecations. Deprecations can only be merged with the "target: minor" or ` + - `"target: major" label.`; - return new this(message); - } - - static hasFeatureCommits(label: TargetLabel) { - const message = `Cannot merge into branch for "${label.pattern}" as the pull request has ` + - 'commits with the "feat" type. New features can only be merged with the "target: minor" ' + - 'or "target: major" label.'; - return new this(message); - } - - static missingBreakingChangeLabel() { - const message = 'Pull Request has at least one commit containing a breaking change note, but ' + - 'does not have a breaking change label.'; - return new this(message); - } - - static missingBreakingChangeCommit() { - const message = 'Pull Request has a breaking change label, but does not contain any commits ' + - 'with breaking change notes.'; - return new this(message); - } -} diff --git a/dev-infra/pr/merge/index.ts b/dev-infra/pr/merge/index.ts deleted file mode 100644 index 76feb3db69e4f2..00000000000000 --- a/dev-infra/pr/merge/index.ts +++ /dev/null @@ -1,145 +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 {getConfig} from '../../utils/config'; -import {error, green, info, promptConfirm, red, yellow} from '../../utils/console'; -import {AuthenticatedGitClient} from '../../utils/git/authenticated-git-client'; -import {GithubApiRequestError} from '../../utils/git/github'; -import {GITHUB_TOKEN_GENERATE_URL} from '../../utils/git/github-urls'; - -import {loadAndValidateConfig, MergeConfigWithRemote} from './config'; -import {MergeResult, MergeStatus, PullRequestMergeTask, PullRequestMergeTaskFlags} from './task'; - -/** - * Merges a given pull request based on labels configured in the given merge configuration. - * Pull requests can be merged with different strategies such as the Github API merge - * strategy, or the local autosquash strategy. Either strategy has benefits and downsides. - * More information on these strategies can be found in their dedicated strategy classes. - * - * See {@link GithubApiMergeStrategy} and {@link AutosquashMergeStrategy} - * - * @param prNumber Number of the pull request that should be merged. - * @param flags Configuration options for merging pull requests. - */ -export async function mergePullRequest(prNumber: number, flags: PullRequestMergeTaskFlags) { - // Set the environment variable to skip all git commit hooks triggered by husky. We are unable to - // rely on `--no-verify` as some hooks still run, notably the `prepare-commit-msg` hook. - process.env['HUSKY'] = '0'; - - const api = await createPullRequestMergeTask(flags); - - // Perform the merge. Force mode can be activated through a command line flag. - // Alternatively, if the merge fails with non-fatal failures, the script - // will prompt whether it should rerun in force mode. - if (!await performMerge(false)) { - process.exit(1); - } - - /** Performs the merge and returns whether it was successful or not. */ - async function performMerge(ignoreFatalErrors: boolean): Promise { - try { - const result = await api.merge(prNumber, ignoreFatalErrors); - return await handleMergeResult(result, ignoreFatalErrors); - } catch (e) { - // Catch errors to the Github API for invalid requests. We want to - // exit the script with a better explanation of the error. - if (e instanceof GithubApiRequestError && e.status === 401) { - error(red('Github API request failed. ' + e.message)); - error(yellow('Please ensure that your provided token is valid.')); - error(yellow(`You can generate a token here: ${GITHUB_TOKEN_GENERATE_URL}`)); - process.exit(1); - } - throw e; - } - } - - /** - * Prompts whether the specified pull request should be forcibly merged. If so, merges - * the specified pull request forcibly (ignoring non-critical failures). - * @returns Whether the specified pull request has been forcibly merged. - */ - async function promptAndPerformForceMerge(): Promise { - if (await promptConfirm('Do you want to forcibly proceed with merging?')) { - // Perform the merge in force mode. This means that non-fatal failures - // are ignored and the merge continues. - return performMerge(true); - } - return false; - } - - /** - * Handles the merge result by printing console messages, exiting the process - * based on the result, or by restarting the merge if force mode has been enabled. - * @returns Whether the merge completed without errors or not. - */ - async function handleMergeResult(result: MergeResult, disableForceMergePrompt = false) { - const {failure, status} = result; - const canForciblyMerge = failure && failure.nonFatal; - - switch (status) { - case MergeStatus.SUCCESS: - info(green(`Successfully merged the pull request: #${prNumber}`)); - return true; - case MergeStatus.DIRTY_WORKING_DIR: - error( - red(`Local working repository not clean. Please make sure there are ` + - `no uncommitted changes.`)); - return false; - case MergeStatus.UNKNOWN_GIT_ERROR: - error( - red('An unknown Git error has been thrown. Please check the output ' + - 'above for details.')); - return false; - case MergeStatus.GITHUB_ERROR: - error(red('An error related to interacting with Github has been discovered.')); - error(failure!.message); - return false; - case MergeStatus.USER_ABORTED: - info(`Merge of pull request has been aborted manually: #${prNumber}`); - return true; - case MergeStatus.FAILED: - error(yellow(`Could not merge the specified pull request.`)); - error(red(failure!.message)); - if (canForciblyMerge && !disableForceMergePrompt) { - info(); - info(yellow('The pull request above failed due to non-critical errors.')); - info(yellow(`This error can be forcibly ignored if desired.`)); - return await promptAndPerformForceMerge(); - } - return false; - default: - throw Error(`Unexpected merge result: ${status}`); - } - } -} - -/** - * Creates the pull request merge task using the given configuration options. Explicit configuration - * options can be specified when the merge script is used outside of an `ng-dev` configured - * repository. - */ -async function createPullRequestMergeTask(flags: PullRequestMergeTaskFlags) { - const devInfraConfig = getConfig(); - /** The singleton instance of the authenticated git client. */ - const git = AuthenticatedGitClient.get(); - const {config, errors} = await loadAndValidateConfig(devInfraConfig, git.github); - - if (errors) { - error(red('Invalid merge configuration:')); - errors.forEach(desc => error(yellow(` - ${desc}`))); - process.exit(1); - } - - // Set the remote so that the merge tool has access to information about - // the remote it intends to merge to. - config!.remote = devInfraConfig.github; - // We can cast this to a merge config with remote because we always set the - // remote above. - return new PullRequestMergeTask(config! as MergeConfigWithRemote, git, flags); -} diff --git a/dev-infra/pr/merge/messages.ts b/dev-infra/pr/merge/messages.ts deleted file mode 100644 index 34a34ce3db5b25..00000000000000 --- a/dev-infra/pr/merge/messages.ts +++ /dev/null @@ -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 {red} from '../../utils/console'; - -import {PullRequest} from './pull-request'; - -export function getCaretakerNotePromptMessage(pullRequest: PullRequest): string { - return red('Pull request has a caretaker note applied. Please make sure you read it.') + - `\nQuick link to PR: ${pullRequest.url}\nDo you want to proceed merging?`; -} - -export function getTargettedBranchesConfirmationPromptMessage(pullRequest: PullRequest): string { - const targetBranchListAsString = pullRequest.targetBranches.map(b => ` - ${b}\n`).join(''); - return `Pull request #${pullRequest.prNumber} will merge into:\n${ - targetBranchListAsString}\nDo you want to proceed merging?`; -} diff --git a/dev-infra/pr/merge/pull-request.ts b/dev-infra/pr/merge/pull-request.ts deleted file mode 100644 index 3137e425f1a383..00000000000000 --- a/dev-infra/pr/merge/pull-request.ts +++ /dev/null @@ -1,273 +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 {params, types as graphqlTypes} from 'typed-graphqlify'; - -import {Commit, parseCommitMessage} from '../../commit-message/parse'; -import {red, warn} from '../../utils/console'; -import {AuthenticatedGitClient} from '../../utils/git/authenticated-git-client'; -import {getPr} from '../../utils/github'; - -import {MergeConfig, TargetLabel} from './config'; -import {PullRequestFailure} from './failures'; -import {matchesPattern} from './string-pattern'; -import {getBranchesFromTargetLabel, getTargetLabelFromPullRequest, InvalidTargetBranchError, InvalidTargetLabelError} from './target-label'; -import {PullRequestMergeTask} from './task'; - -/** The default label for labeling pull requests containing a breaking change. */ -const BreakingChangeLabel = 'breaking changes'; - -/** Interface that describes a pull request. */ -export interface PullRequest { - /** URL to the pull request. */ - url: string; - /** Number of the pull request. */ - prNumber: number; - /** Title of the pull request. */ - title: string; - /** Labels applied to the pull request. */ - labels: string[]; - /** List of branches this PR should be merged into. */ - targetBranches: string[]; - /** Branch that the PR targets in the Github UI. */ - githubTargetBranch: string; - /** Count of commits in this pull request. */ - commitCount: number; - /** Optional SHA that this pull request needs to be based on. */ - requiredBaseSha?: string; - /** Whether the pull request commit message fixup. */ - needsCommitMessageFixup: boolean; - /** Whether the pull request has a caretaker note. */ - hasCaretakerNote: boolean; -} - -/** - * Loads and validates the specified pull request against the given configuration. - * If the pull requests fails, a pull request failure is returned. - */ -export async function loadAndValidatePullRequest( - {git, config}: PullRequestMergeTask, prNumber: number, - ignoreNonFatalFailures = false): Promise { - const prData = await fetchPullRequestFromGithub(git, prNumber); - - if (prData === null) { - return PullRequestFailure.notFound(); - } - - const labels = prData.labels.nodes.map(l => l.name); - - if (!labels.some(name => matchesPattern(name, config.mergeReadyLabel))) { - return PullRequestFailure.notMergeReady(); - } - if (!labels.some(name => matchesPattern(name, config.claSignedLabel))) { - return PullRequestFailure.claUnsigned(); - } - - let targetLabel: TargetLabel; - try { - targetLabel = getTargetLabelFromPullRequest(config, labels); - } catch (error) { - if (error instanceof InvalidTargetLabelError) { - return new PullRequestFailure(error.failureMessage); - } - throw error; - } - - /** List of parsed commits for all of the commits in the pull request. */ - const commitsInPr = prData.commits.nodes.map(n => parseCommitMessage(n.commit.message)); - - try { - assertPendingState(prData); - assertChangesAllowForTargetLabel(commitsInPr, targetLabel, config); - assertCorrectBreakingChangeLabeling(commitsInPr, labels, config); - } catch (error) { - return error; - } - - /** The combined status of the latest commit in the pull request. */ - const state = prData.commits.nodes.slice(-1)[0].commit.status.state; - if (state === 'FAILURE' && !ignoreNonFatalFailures) { - return PullRequestFailure.failingCiJobs(); - } - if (state === 'PENDING' && !ignoreNonFatalFailures) { - return PullRequestFailure.pendingCiJobs(); - } - - const githubTargetBranch = prData.baseRefName; - const requiredBaseSha = - config.requiredBaseCommits && config.requiredBaseCommits[githubTargetBranch]; - const needsCommitMessageFixup = !!config.commitMessageFixupLabel && - labels.some(name => matchesPattern(name, config.commitMessageFixupLabel)); - const hasCaretakerNote = !!config.caretakerNoteLabel && - labels.some(name => matchesPattern(name, config.caretakerNoteLabel!)); - let targetBranches: string[]; - - // If branches are determined for a given target label, capture errors that are - // thrown as part of branch computation. This is expected because a merge configuration - // can lazily compute branches for a target label and throw. e.g. if an invalid target - // label is applied, we want to exit the script gracefully with an error message. - try { - targetBranches = await getBranchesFromTargetLabel(targetLabel, githubTargetBranch); - } catch (error) { - if (error instanceof InvalidTargetBranchError || error instanceof InvalidTargetLabelError) { - return new PullRequestFailure(error.failureMessage); - } - throw error; - } - - return { - url: prData.url, - prNumber, - labels, - requiredBaseSha, - githubTargetBranch, - needsCommitMessageFixup, - hasCaretakerNote, - targetBranches, - title: prData.title, - commitCount: prData.commits.totalCount, - }; -} - -/* Graphql schema for the response body the requested pull request. */ -const PR_SCHEMA = { - url: graphqlTypes.string, - isDraft: graphqlTypes.boolean, - state: graphqlTypes.oneOf(['OPEN', 'MERGED', 'CLOSED'] as const), - number: graphqlTypes.number, - // Only the last 100 commits from a pull request are obtained as we likely will never see a pull - // requests with more than 100 commits. - commits: params({last: 100}, { - totalCount: graphqlTypes.number, - nodes: [{ - commit: { - status: { - state: graphqlTypes.oneOf(['FAILURE', 'PENDING', 'SUCCESS'] as const), - }, - message: graphqlTypes.string, - }, - }], - }), - baseRefName: graphqlTypes.string, - title: graphqlTypes.string, - labels: params({first: 100}, { - nodes: [{ - name: graphqlTypes.string, - }] - }), -}; - -/** A pull request retrieved from github via the graphql API. */ -type RawPullRequest = typeof PR_SCHEMA; - - -/** Fetches a pull request from Github. Returns null if an error occurred. */ -async function fetchPullRequestFromGithub( - git: AuthenticatedGitClient, prNumber: number): Promise { - try { - return await getPr(PR_SCHEMA, prNumber, git); - } catch (e) { - // If the pull request could not be found, we want to return `null` so - // that the error can be handled gracefully. - if (e.status === 404) { - return null; - } - throw e; - } -} - -/** Whether the specified value resolves to a pull request. */ -export function isPullRequest(v: PullRequestFailure|PullRequest): v is PullRequest { - return (v as PullRequest).targetBranches !== undefined; -} - -/** - * Assert the commits provided are allowed to merge to the provided target label, - * throwing an error otherwise. - * @throws {PullRequestFailure} - */ -function assertChangesAllowForTargetLabel( - commits: Commit[], label: TargetLabel, config: MergeConfig) { - /** - * List of commit scopes which are exempted from target label content requirements. i.e. no `feat` - * scopes in patch branches, no breaking changes in minor or patch changes. - */ - const exemptedScopes = config.targetLabelExemptScopes || []; - /** List of commits which are subject to content requirements for the target label. */ - commits = commits.filter(commit => !exemptedScopes.includes(commit.scope)); - const hasBreakingChanges = commits.some(commit => commit.breakingChanges.length !== 0); - const hasDeprecations = commits.some(commit => commit.deprecations.length !== 0); - const hasFeatureCommits = commits.some(commit => commit.type === 'feat'); - switch (label.pattern) { - case 'target: major': - break; - case 'target: minor': - if (hasBreakingChanges) { - throw PullRequestFailure.hasBreakingChanges(label); - } - break; - case 'target: rc': - case 'target: patch': - case 'target: lts': - if (hasBreakingChanges) { - throw PullRequestFailure.hasBreakingChanges(label); - } - if (hasFeatureCommits) { - throw PullRequestFailure.hasFeatureCommits(label); - } - // Deprecations should not be merged into RC, patch or LTS branches. - // https://semver.org/#spec-item-7. Deprecations should be part of - // minor releases, or major releases according to SemVer. - if (hasDeprecations) { - throw PullRequestFailure.hasDeprecations(label); - } - break; - default: - warn(red('WARNING: Unable to confirm all commits in the pull request are eligible to be')); - warn(red(`merged into the target branch: ${label.pattern}`)); - break; - } -} - -/** - * Assert the pull request has the proper label for breaking changes if there are breaking change - * commits, and only has the label if there are breaking change commits. - * @throws {PullRequestFailure} - */ -function assertCorrectBreakingChangeLabeling( - commits: Commit[], labels: string[], config: MergeConfig) { - /** Whether the PR has a label noting a breaking change. */ - const hasLabel = labels.includes(config.breakingChangeLabel || BreakingChangeLabel); - //** Whether the PR has at least one commit which notes a breaking change. */ - const hasCommit = commits.some(commit => commit.breakingChanges.length !== 0); - - if (!hasLabel && hasCommit) { - throw PullRequestFailure.missingBreakingChangeLabel(); - } - - if (hasLabel && !hasCommit) { - throw PullRequestFailure.missingBreakingChangeCommit(); - } -} - - -/** - * Assert the pull request is pending, not closed, merged or in draft. - * @throws {PullRequestFailure} if the pull request is not pending. - */ -function assertPendingState(pr: RawPullRequest) { - if (pr.isDraft) { - throw PullRequestFailure.isDraft(); - } - switch (pr.state) { - case 'CLOSED': - throw PullRequestFailure.isClosed(); - case 'MERGED': - throw PullRequestFailure.isMerged(); - } -} diff --git a/dev-infra/pr/merge/strategies/api-merge.ts b/dev-infra/pr/merge/strategies/api-merge.ts deleted file mode 100644 index 49f2cc3cf787d7..00000000000000 --- a/dev-infra/pr/merge/strategies/api-merge.ts +++ /dev/null @@ -1,234 +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 {RestEndpointMethodTypes} from '@octokit/plugin-rest-endpoint-methods'; -import {prompt} from 'inquirer'; - -import {parseCommitMessage} from '../../../commit-message/parse'; -import {AuthenticatedGitClient} from '../../../utils/git/authenticated-git-client'; -import {GithubApiMergeMethod} from '../config'; -import {PullRequestFailure} from '../failures'; -import {PullRequest} from '../pull-request'; -import {matchesPattern} from '../string-pattern'; - -import {MergeStrategy, TEMP_PR_HEAD_BRANCH} from './strategy'; - -/** Type describing the parameters for the Octokit `merge` API endpoint. */ -type OctokitMergeParams = RestEndpointMethodTypes['pulls']['merge']['parameters']; - -/** Configuration for the Github API merge strategy. */ -export interface GithubApiMergeStrategyConfig { - /** Default method used for merging pull requests */ - default: GithubApiMergeMethod; - /** Labels which specify a different merge method than the default. */ - labels?: {pattern: string, method: GithubApiMergeMethod}[]; -} - -/** Separator between commit message header and body. */ -const COMMIT_HEADER_SEPARATOR = '\n\n'; - -/** - * Merge strategy that primarily leverages the Github API. The strategy merges a given - * pull request into a target branch using the API. This ensures that Github displays - * the pull request as merged. The merged commits are then cherry-picked into the remaining - * target branches using the local Git instance. The benefit is that the Github merged state - * is properly set, but a notable downside is that PRs cannot use fixup or squash commits. - */ -export class GithubApiMergeStrategy extends MergeStrategy { - constructor(git: AuthenticatedGitClient, private _config: GithubApiMergeStrategyConfig) { - super(git); - } - - override async merge(pullRequest: PullRequest): Promise { - const {githubTargetBranch, prNumber, targetBranches, requiredBaseSha, needsCommitMessageFixup} = - pullRequest; - // If the pull request does not have its base branch set to any determined target - // branch, we cannot merge using the API. - if (targetBranches.every(t => t !== githubTargetBranch)) { - return PullRequestFailure.mismatchingTargetBranch(targetBranches); - } - - // In cases where a required base commit is specified for this pull request, check if - // the pull request contains the given commit. If not, return a pull request failure. - // This check is useful for enforcing that PRs are rebased on top of a given commit. - // e.g. a commit that changes the code ownership validation. PRs which are not rebased - // could bypass new codeowner ship rules. - if (requiredBaseSha && !this.git.hasCommit(TEMP_PR_HEAD_BRANCH, requiredBaseSha)) { - return PullRequestFailure.unsatisfiedBaseSha(); - } - - const method = this._getMergeActionFromPullRequest(pullRequest); - const cherryPickTargetBranches = targetBranches.filter(b => b !== githubTargetBranch); - - // First cherry-pick the PR into all local target branches in dry-run mode. This is - // purely for testing so that we can figure out whether the PR can be cherry-picked - // into the other target branches. We don't want to merge the PR through the API, and - // then run into cherry-pick conflicts after the initial merge already completed. - const failure = await this._checkMergability(pullRequest, cherryPickTargetBranches); - - // If the PR could not be cherry-picked into all target branches locally, we know it can't - // be done through the Github API either. We abort merging and pass-through the failure. - if (failure !== null) { - return failure; - } - - const mergeOptions: OctokitMergeParams = { - pull_number: prNumber, - merge_method: method, - ...this.git.remoteParams, - }; - - if (needsCommitMessageFixup) { - // Commit message fixup does not work with other merge methods as the Github API only - // allows commit message modifications for squash merging. - if (method !== 'squash') { - return PullRequestFailure.unableToFixupCommitMessageSquashOnly(); - } - await this._promptCommitMessageEdit(pullRequest, mergeOptions); - } - - let mergeStatusCode: number; - let targetSha: string; - - try { - // Merge the pull request using the Github API into the selected base branch. - const result = await this.git.github.pulls.merge(mergeOptions); - - mergeStatusCode = result.status; - targetSha = result.data.sha; - } catch (e) { - // Note: Github usually returns `404` as status code if the API request uses a - // token with insufficient permissions. Github does this because it doesn't want - // to leak whether a repository exists or not. In our case we expect a certain - // repository to exist, so we always treat this as a permission failure. - if (e.status === 403 || e.status === 404) { - return PullRequestFailure.insufficientPermissionsToMerge(); - } - throw e; - } - - // https://developer.github.com/v3/pulls/#response-if-merge-cannot-be-performed - // Pull request cannot be merged due to merge conflicts. - if (mergeStatusCode === 405) { - return PullRequestFailure.mergeConflicts([githubTargetBranch]); - } - if (mergeStatusCode !== 200) { - return PullRequestFailure.unknownMergeError(); - } - - // If the PR does not need to be merged into any other target branches, - // we exit here as we already completed the merge. - if (!cherryPickTargetBranches.length) { - return null; - } - - // Refresh the target branch the PR has been merged into through the API. We need - // to re-fetch as otherwise we cannot cherry-pick the new commits into the remaining - // target branches. - this.fetchTargetBranches([githubTargetBranch]); - - // Number of commits that have landed in the target branch. This could vary from - // the count of commits in the PR due to squashing. - const targetCommitsCount = method === 'squash' ? 1 : pullRequest.commitCount; - - // Cherry pick the merged commits into the remaining target branches. - const failedBranches = await this.cherryPickIntoTargetBranches( - `${targetSha}~${targetCommitsCount}..${targetSha}`, cherryPickTargetBranches, { - // Commits that have been created by the Github API do not necessarily contain - // a reference to the source pull request (unless the squash strategy is used). - // To ensure that original commits can be found when a commit is viewed in a - // target branch, we add a link to the original commits when cherry-picking. - linkToOriginalCommits: true, - }); - - // We already checked whether the PR can be cherry-picked into the target branches, - // but in case the cherry-pick somehow fails, we still handle the conflicts here. The - // commits created through the Github API could be different (i.e. through squash). - if (failedBranches.length) { - return PullRequestFailure.mergeConflicts(failedBranches); - } - - this.pushTargetBranchesUpstream(cherryPickTargetBranches); - return null; - } - - /** - * Prompts the user for the commit message changes. Unlike as in the autosquash merge - * strategy, we cannot start an interactive rebase because we merge using the Github API. - * The Github API only allows modifications to PR title and body for squash merges. - */ - private async _promptCommitMessageEdit( - pullRequest: PullRequest, mergeOptions: OctokitMergeParams) { - const commitMessage = await this._getDefaultSquashCommitMessage(pullRequest); - const {result} = await prompt<{result: string}>({ - type: 'editor', - name: 'result', - message: 'Please update the commit message', - default: commitMessage, - }); - - // Split the new message into title and message. This is necessary because the - // Github API expects title and message to be passed separately. - const [newTitle, ...newMessage] = result.split(COMMIT_HEADER_SEPARATOR); - - // Update the merge options so that the changes are reflected in there. - mergeOptions.commit_title = `${newTitle} (#${pullRequest.prNumber})`; - mergeOptions.commit_message = newMessage.join(COMMIT_HEADER_SEPARATOR); - } - - /** - * Gets a commit message for the given pull request. Github by default concatenates - * multiple commit messages if a PR is merged in squash mode. We try to replicate this - * behavior here so that we have a default commit message that can be fixed up. - */ - private async _getDefaultSquashCommitMessage(pullRequest: PullRequest): Promise { - const commits = (await this._getPullRequestCommitMessages(pullRequest)) - .map(message => ({message, parsed: parseCommitMessage(message)})); - const messageBase = `${pullRequest.title}${COMMIT_HEADER_SEPARATOR}`; - if (commits.length <= 1) { - return `${messageBase}${commits[0].parsed.body}`; - } - const joinedMessages = commits.map(c => `* ${c.message}`).join(COMMIT_HEADER_SEPARATOR); - return `${messageBase}${joinedMessages}`; - } - - /** Gets all commit messages of commits in the pull request. */ - private async _getPullRequestCommitMessages({prNumber}: PullRequest) { - const allCommits = await this.git.github.paginate( - this.git.github.pulls.listCommits, {...this.git.remoteParams, pull_number: prNumber}); - return allCommits.map(({commit}) => commit.message); - } - - /** - * Checks if given pull request could be merged into its target branches. - * @returns A pull request failure if it the PR could not be merged. - */ - private async _checkMergability(pullRequest: PullRequest, targetBranches: string[]): - Promise { - const revisionRange = this.getPullRequestRevisionRange(pullRequest); - const failedBranches = - this.cherryPickIntoTargetBranches(revisionRange, targetBranches, {dryRun: true}); - - if (failedBranches.length) { - return PullRequestFailure.mergeConflicts(failedBranches); - } - return null; - } - - /** Determines the merge action from the given pull request. */ - private _getMergeActionFromPullRequest({labels}: PullRequest): GithubApiMergeMethod { - if (this._config.labels) { - const matchingLabel = - this._config.labels.find(({pattern}) => labels.some(l => matchesPattern(l, pattern))); - if (matchingLabel !== undefined) { - return matchingLabel.method; - } - } - return this._config.default; - } -} diff --git a/dev-infra/pr/merge/strategies/autosquash-merge.ts b/dev-infra/pr/merge/strategies/autosquash-merge.ts deleted file mode 100644 index bff4e518e94a90..00000000000000 --- a/dev-infra/pr/merge/strategies/autosquash-merge.ts +++ /dev/null @@ -1,113 +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 {PullRequestFailure} from '../failures'; -import {PullRequest} from '../pull-request'; -import {MergeStrategy, TEMP_PR_HEAD_BRANCH} from './strategy'; - -/** Path to the commit message filter script. Git expects this paths to use forward slashes. */ -const MSG_FILTER_SCRIPT = join(__dirname, './commit-message-filter.js').replace(/\\/g, '/'); - -/** - * Merge strategy that does not use the Github API for merging. Instead, it fetches - * all target branches and the PR locally. The PR is then cherry-picked with autosquash - * enabled into the target branches. The benefit is the support for fixup and squash commits. - * A notable downside though is that Github does not show the PR as `Merged` due to non - * fast-forward merges - */ -export class AutosquashMergeStrategy extends MergeStrategy { - /** - * Merges the specified pull request into the target branches and pushes the target - * branches upstream. This method requires the temporary target branches to be fetched - * already as we don't want to fetch the target branches per pull request merge. This - * would causes unnecessary multiple fetch requests when multiple PRs are merged. - * @throws {GitCommandError} An unknown Git command error occurred that is not - * specific to the pull request merge. - * @returns A pull request failure or null in case of success. - */ - override async merge(pullRequest: PullRequest): Promise { - const {prNumber, targetBranches, requiredBaseSha, needsCommitMessageFixup, githubTargetBranch} = - pullRequest; - // In case a required base is specified for this pull request, check if the pull - // request contains the given commit. If not, return a pull request failure. This - // check is useful for enforcing that PRs are rebased on top of a given commit. e.g. - // a commit that changes the codeowner ship validation. PRs which are not rebased - // could bypass new codeowner ship rules. - if (requiredBaseSha && !this.git.hasCommit(TEMP_PR_HEAD_BRANCH, requiredBaseSha)) { - return PullRequestFailure.unsatisfiedBaseSha(); - } - - // SHA for the first commit the pull request is based on. Usually we would able - // to just rely on the base revision provided by `getPullRequestBaseRevision`, but - // the revision would rely on the amount of commits in a pull request. This is not - // reliable as we rebase the PR with autosquash where the amount of commits could - // change. We work around this by parsing the base revision so that we have a fixated - // SHA before the autosquash rebase is performed. - const baseSha = - this.git.run(['rev-parse', this.getPullRequestBaseRevision(pullRequest)]).stdout.trim(); - // Git revision range that matches the pull request commits. - const revisionRange = `${baseSha}..${TEMP_PR_HEAD_BRANCH}`; - - // We always rebase the pull request so that fixup or squash commits are automatically - // collapsed. Git's autosquash functionality does only work in interactive rebases, so - // our rebase is always interactive. In reality though, unless a commit message fixup - // is desired, we set the `GIT_SEQUENCE_EDITOR` environment variable to `true` so that - // the rebase seems interactive to Git, while it's not interactive to the user. - // See: https://github.com/git/git/commit/891d4a0313edc03f7e2ecb96edec5d30dc182294. - const branchOrRevisionBeforeRebase = this.git.getCurrentBranchOrRevision(); - const rebaseEnv = - needsCommitMessageFixup ? undefined : {...process.env, GIT_SEQUENCE_EDITOR: 'true'}; - this.git.run( - ['rebase', '--interactive', '--autosquash', baseSha, TEMP_PR_HEAD_BRANCH], - {stdio: 'inherit', env: rebaseEnv}); - - // Update pull requests commits to reference the pull request. This matches what - // Github does when pull requests are merged through the Web UI. The motivation is - // that it should be easy to determine which pull request contained a given commit. - // Note: The filter-branch command relies on the working tree, so we want to make sure - // that we are on the initial branch or revision where the merge script has been invoked. - this.git.run(['checkout', '-f', branchOrRevisionBeforeRebase]); - this.git.run( - ['filter-branch', '-f', '--msg-filter', `${MSG_FILTER_SCRIPT} ${prNumber}`, revisionRange]); - - // Cherry-pick the pull request into all determined target branches. - const failedBranches = this.cherryPickIntoTargetBranches(revisionRange, targetBranches); - - if (failedBranches.length) { - return PullRequestFailure.mergeConflicts(failedBranches); - } - - this.pushTargetBranchesUpstream(targetBranches); - - // For PRs which do not target the `master` branch on Github, Github does not automatically - // close the PR when its commit is pushed into the repository. To ensure these PRs are - // correctly marked as closed, we must detect this situation and close the PR via the API after - // the upstream pushes are completed. - if (githubTargetBranch !== 'master') { - /** The local branch name of the github targeted branch. */ - const localBranch = this.getLocalTargetBranchName(githubTargetBranch); - /** The SHA of the commit pushed to github which represents closing the PR. */ - const sha = this.git.run(['rev-parse', localBranch]).stdout.trim(); - // Create a comment saying the PR was closed by the SHA. - await this.git.github.issues.createComment({ - ...this.git.remoteParams, - issue_number: pullRequest.prNumber, - body: `Closed by commit ${sha}` - }); - // Actually close the PR. - await this.git.github.pulls.update({ - ...this.git.remoteParams, - pull_number: pullRequest.prNumber, - state: 'closed', - }); - } - - return null; - } -} diff --git a/dev-infra/pr/merge/strategies/commit-message-filter.js b/dev-infra/pr/merge/strategies/commit-message-filter.js deleted file mode 100755 index 5503b5b1e32df1..00000000000000 --- a/dev-infra/pr/merge/strategies/commit-message-filter.js +++ /dev/null @@ -1,45 +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 - */ - -/** - * Script that can be passed as commit message filter to `git filter-branch --msg-filter`. - * The script rewrites commit messages to contain a Github instruction to close the - * corresponding pull request. For more details. See: https://git.io/Jv64r. - */ - -if (require.main === module) { - const [prNumber] = process.argv.slice(2); - if (!prNumber) { - console.error('No pull request number specified.'); - process.exit(1); - } - - let commitMessage = ''; - process.stdin.setEncoding('utf8'); - process.stdin.on('readable', () => { - const chunk = process.stdin.read(); - if (chunk !== null) { - commitMessage += chunk; - } - }); - - process.stdin.on('end', () => { - console.info(rewriteCommitMessage(commitMessage, prNumber)); - }); -} - -function rewriteCommitMessage(message, prNumber) { - const lines = message.split(/\n/); - // Add the pull request number to the commit message title. This matches what - // Github does when PRs are merged on the web through the `Squash and Merge` button. - lines[0] += ` (#${prNumber})`; - // Push a new line that instructs Github to close the specified pull request. - lines.push(`PR Close #${prNumber}`); - return lines.join('\n'); -} diff --git a/dev-infra/pr/merge/strategies/strategy.ts b/dev-infra/pr/merge/strategies/strategy.ts deleted file mode 100644 index 0011b5b2e36021..00000000000000 --- a/dev-infra/pr/merge/strategies/strategy.ts +++ /dev/null @@ -1,141 +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 {AuthenticatedGitClient} from '../../../utils/git/authenticated-git-client'; -import {PullRequestFailure} from '../failures'; -import {PullRequest} from '../pull-request'; - -/** - * Name of a temporary branch that contains the head of a currently-processed PR. Note - * that a branch name should be used that most likely does not conflict with other local - * development branches. - */ -export const TEMP_PR_HEAD_BRANCH = 'merge_pr_head'; - -/** - * Base class for merge strategies. A merge strategy accepts a pull request and - * merges it into the determined target branches. - */ -export abstract class MergeStrategy { - constructor(protected git: AuthenticatedGitClient) {} - - /** - * Prepares a merge of the given pull request. The strategy by default will - * fetch all target branches and the pull request into local temporary branches. - */ - async prepare(pullRequest: PullRequest) { - this.fetchTargetBranches( - pullRequest.targetBranches, `pull/${pullRequest.prNumber}/head:${TEMP_PR_HEAD_BRANCH}`); - } - - /** - * Performs the merge of the given pull request. This needs to be implemented - * by individual merge strategies. - */ - abstract merge(pullRequest: PullRequest): Promise; - - /** Cleans up the pull request merge. e.g. deleting temporary local branches. */ - async cleanup(pullRequest: PullRequest) { - // Delete all temporary target branches. - pullRequest.targetBranches.forEach( - branchName => this.git.run(['branch', '-D', this.getLocalTargetBranchName(branchName)])); - - // Delete temporary branch for the pull request head. - this.git.run(['branch', '-D', TEMP_PR_HEAD_BRANCH]); - } - - /** Gets the revision range for all commits in the given pull request. */ - protected getPullRequestRevisionRange(pullRequest: PullRequest): string { - return `${this.getPullRequestBaseRevision(pullRequest)}..${TEMP_PR_HEAD_BRANCH}`; - } - - /** Gets the base revision of a pull request. i.e. the commit the PR is based on. */ - protected getPullRequestBaseRevision(pullRequest: PullRequest): string { - return `${TEMP_PR_HEAD_BRANCH}~${pullRequest.commitCount}`; - } - - /** Gets a deterministic local branch name for a given branch. */ - protected getLocalTargetBranchName(targetBranch: string): string { - return `merge_pr_target_${targetBranch.replace(/\//g, '_')}`; - } - - /** - * Cherry-picks the given revision range into the specified target branches. - * @returns A list of branches for which the revisions could not be cherry-picked into. - */ - protected cherryPickIntoTargetBranches(revisionRange: string, targetBranches: string[], options: { - dryRun?: boolean, - linkToOriginalCommits?: boolean, - } = {}) { - const cherryPickArgs = [revisionRange]; - const failedBranches: string[] = []; - - if (options.dryRun) { - // https://git-scm.com/docs/git-cherry-pick#Documentation/git-cherry-pick.txt---no-commit - // This causes `git cherry-pick` to not generate any commits. Instead, the changes are - // applied directly in the working tree. This allow us to easily discard the changes - // for dry-run purposes. - cherryPickArgs.push('--no-commit'); - } - - if (options.linkToOriginalCommits) { - // We add `-x` when cherry-picking as that will allow us to easily jump to original - // commits for cherry-picked commits. With that flag set, Git will automatically append - // the original SHA/revision to the commit message. e.g. `(cherry picked from commit <..>)`. - // https://git-scm.com/docs/git-cherry-pick#Documentation/git-cherry-pick.txt--x. - cherryPickArgs.push('-x'); - } - - // Cherry-pick the refspec into all determined target branches. - for (const branchName of targetBranches) { - const localTargetBranch = this.getLocalTargetBranchName(branchName); - // Checkout the local target branch. - this.git.run(['checkout', localTargetBranch]); - // Cherry-pick the refspec into the target branch. - if (this.git.runGraceful(['cherry-pick', ...cherryPickArgs]).status !== 0) { - // Abort the failed cherry-pick. We do this because Git persists the failed - // cherry-pick state globally in the repository. This could prevent future - // pull request merges as a Git thinks a cherry-pick is still in progress. - this.git.runGraceful(['cherry-pick', '--abort']); - failedBranches.push(branchName); - } - // If we run with dry run mode, we reset the local target branch so that all dry-run - // cherry-pick changes are discard. Changes are applied to the working tree and index. - if (options.dryRun) { - this.git.run(['reset', '--hard', 'HEAD']); - } - } - return failedBranches; - } - - /** - * Fetches the given target branches. Also accepts a list of additional refspecs that - * should be fetched. This is helpful as multiple slow fetches could be avoided. - */ - protected fetchTargetBranches(names: string[], ...extraRefspecs: string[]) { - const fetchRefspecs = names.map(targetBranch => { - const localTargetBranch = this.getLocalTargetBranchName(targetBranch); - return `refs/heads/${targetBranch}:${localTargetBranch}`; - }); - // Fetch all target branches with a single command. We don't want to fetch them - // individually as that could cause an unnecessary slow-down. - this.git.run( - ['fetch', '-q', '-f', this.git.getRepoGitUrl(), ...fetchRefspecs, ...extraRefspecs]); - } - - /** Pushes the given target branches upstream. */ - protected pushTargetBranchesUpstream(names: string[]) { - const pushRefspecs = names.map(targetBranch => { - const localTargetBranch = this.getLocalTargetBranchName(targetBranch); - return `${localTargetBranch}:refs/heads/${targetBranch}`; - }); - // Push all target branches with a single command if we don't run in dry-run mode. - // We don't want to push them individually as that could cause an unnecessary slow-down. - this.git.run(['push', this.git.getRepoGitUrl(), ...pushRefspecs]); - } -} diff --git a/dev-infra/pr/merge/string-pattern.ts b/dev-infra/pr/merge/string-pattern.ts deleted file mode 100644 index 01c9c7122ff47e..00000000000000 --- a/dev-infra/pr/merge/string-pattern.ts +++ /dev/null @@ -1,12 +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 - */ - -/** Checks whether the specified value matches the given pattern. */ -export function matchesPattern(value: string, pattern: RegExp|string): boolean { - return typeof pattern === 'string' ? value === pattern : pattern.test(value); -} diff --git a/dev-infra/pr/merge/target-label.ts b/dev-infra/pr/merge/target-label.ts deleted file mode 100644 index 707676306a05f1..00000000000000 --- a/dev-infra/pr/merge/target-label.ts +++ /dev/null @@ -1,60 +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 {MergeConfig, TargetLabel} from './config'; -import {matchesPattern} from './string-pattern'; - -/** - * Unique error that can be thrown in the merge configuration if an - * invalid branch is targeted. - */ -export class InvalidTargetBranchError { - constructor(public failureMessage: string) {} -} - -/** - * Unique error that can be thrown in the merge configuration if an - * invalid label has been applied to a pull request. - */ -export class InvalidTargetLabelError { - constructor(public failureMessage: string) {} -} - -/** Gets the target label from the specified pull request labels. */ -export function getTargetLabelFromPullRequest( - config: Pick, labels: string[]): TargetLabel { - /** List of discovered target labels for the PR. */ - const matches = []; - for (const label of labels) { - const match = config.labels.find(({pattern}) => matchesPattern(label, pattern)); - if (match !== undefined) { - matches.push(match); - } - } - if (matches.length === 1) { - return matches[0]; - } - if (matches.length === 0) { - throw new InvalidTargetLabelError( - 'Unable to determine target for the PR as it has no target label.'); - } - throw new InvalidTargetLabelError( - 'Unable to determine target for the PR as it has multiple target labels.'); -} - -/** - * Gets the branches from the specified target label. - * - * @throws {InvalidTargetLabelError} Invalid label has been applied to pull request. - * @throws {InvalidTargetBranchError} Invalid Github target branch has been selected. - */ -export async function getBranchesFromTargetLabel( - label: TargetLabel, githubTargetBranch: string): Promise { - return typeof label.branches === 'function' ? await label.branches(githubTargetBranch) : - await label.branches; -} diff --git a/dev-infra/pr/merge/task.ts b/dev-infra/pr/merge/task.ts deleted file mode 100644 index 2d6ec9c66f31de..00000000000000 --- a/dev-infra/pr/merge/task.ts +++ /dev/null @@ -1,165 +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 {promptConfirm} from '../../utils/console'; -import {AuthenticatedGitClient} from '../../utils/git/authenticated-git-client'; -import {GitCommandError} from '../../utils/git/git-client'; - -import {MergeConfigWithRemote} from './config'; -import {PullRequestFailure} from './failures'; -import {getCaretakerNotePromptMessage, getTargettedBranchesConfirmationPromptMessage} from './messages'; -import {isPullRequest, loadAndValidatePullRequest,} from './pull-request'; -import {GithubApiMergeStrategy} from './strategies/api-merge'; -import {AutosquashMergeStrategy} from './strategies/autosquash-merge'; - -/** Describes the status of a pull request merge. */ -export const enum MergeStatus { - UNKNOWN_GIT_ERROR, - DIRTY_WORKING_DIR, - SUCCESS, - FAILED, - USER_ABORTED, - GITHUB_ERROR, -} - -/** Result of a pull request merge. */ -export interface MergeResult { - /** Overall status of the merge. */ - status: MergeStatus; - /** List of pull request failures. */ - failure?: PullRequestFailure; -} - -export interface PullRequestMergeTaskFlags { - branchPrompt: boolean; -} - -const defaultPullRequestMergeTaskFlags: PullRequestMergeTaskFlags = { - branchPrompt: true, -}; - -/** - * Class that accepts a merge script configuration and Github token. It provides - * a programmatic interface for merging multiple pull requests based on their - * labels that have been resolved through the merge script configuration. - */ -export class PullRequestMergeTask { - private flags: PullRequestMergeTaskFlags; - - constructor( - public config: MergeConfigWithRemote, public git: AuthenticatedGitClient, - flags: Partial) { - // Update flags property with the provided flags values as patches to the default flag values. - this.flags = {...defaultPullRequestMergeTaskFlags, ...flags}; - } - - /** - * Merges the given pull request and pushes it upstream. - * @param prNumber Pull request that should be merged. - * @param force Whether non-critical pull request failures should be ignored. - */ - async merge(prNumber: number, force = false): Promise { - // Check whether the given Github token has sufficient permissions for writing - // to the configured repository. If the repository is not private, only the - // reduced `public_repo` OAuth scope is sufficient for performing merges. - const hasOauthScopes = await this.git.hasOauthScopes((scopes, missing) => { - if (!scopes.includes('repo')) { - if (this.config.remote.private) { - missing.push('repo'); - } else if (!scopes.includes('public_repo')) { - missing.push('public_repo'); - } - } - - // Pull requests can modify Github action workflow files. In such cases Github requires us to - // push with a token that has the `workflow` oauth scope set. To avoid errors when the - // caretaker intends to merge such PRs, we ensure the scope is always set on the token before - // the merge process starts. - // https://docs.github.com/en/developers/apps/scopes-for-oauth-apps#available-scopes - if (!scopes.includes('workflow')) { - missing.push('workflow'); - } - }); - - if (hasOauthScopes !== true) { - return { - status: MergeStatus.GITHUB_ERROR, - failure: PullRequestFailure.insufficientPermissionsToMerge(hasOauthScopes.error) - }; - } - - if (this.git.hasUncommittedChanges()) { - return {status: MergeStatus.DIRTY_WORKING_DIR}; - } - - const pullRequest = await loadAndValidatePullRequest(this, prNumber, force); - - if (!isPullRequest(pullRequest)) { - return {status: MergeStatus.FAILED, failure: pullRequest}; - } - - - if (this.flags.branchPrompt && - !await promptConfirm(getTargettedBranchesConfirmationPromptMessage(pullRequest))) { - return {status: MergeStatus.USER_ABORTED}; - } - - - // If the pull request has a caretaker note applied, raise awareness by prompting - // the caretaker. The caretaker can then decide to proceed or abort the merge. - if (pullRequest.hasCaretakerNote && - !await promptConfirm(getCaretakerNotePromptMessage(pullRequest))) { - return {status: MergeStatus.USER_ABORTED}; - } - - const strategy = this.config.githubApiMerge ? - new GithubApiMergeStrategy(this.git, this.config.githubApiMerge) : - new AutosquashMergeStrategy(this.git); - - // Branch or revision that is currently checked out so that we can switch back to - // it once the pull request has been merged. - let previousBranchOrRevision: null|string = null; - - // The following block runs Git commands as child processes. These Git commands can fail. - // We want to capture these command errors and return an appropriate merge request status. - try { - previousBranchOrRevision = this.git.getCurrentBranchOrRevision(); - - // Run preparations for the merge (e.g. fetching branches). - await strategy.prepare(pullRequest); - - // Perform the merge and capture potential failures. - const failure = await strategy.merge(pullRequest); - if (failure !== null) { - return {status: MergeStatus.FAILED, failure}; - } - - // Switch back to the previous branch. We need to do this before deleting the temporary - // branches because we cannot delete branches which are currently checked out. - this.git.run(['checkout', '-f', previousBranchOrRevision]); - - await strategy.cleanup(pullRequest); - - // Return a successful merge status. - return {status: MergeStatus.SUCCESS}; - } catch (e) { - // Catch all git command errors and return a merge result w/ git error status code. - // Other unknown errors which aren't caused by a git command are re-thrown. - if (e instanceof GitCommandError) { - return {status: MergeStatus.UNKNOWN_GIT_ERROR}; - } - throw e; - } finally { - // Always try to restore the branch if possible. We don't want to leave - // the repository in a different state than before. - if (previousBranchOrRevision !== null) { - this.git.runGraceful(['checkout', '-f', previousBranchOrRevision]); - } - } - } -} diff --git a/dev-infra/pr/rebase/BUILD.bazel b/dev-infra/pr/rebase/BUILD.bazel deleted file mode 100644 index 9d1d58a2dba1c7..00000000000000 --- a/dev-infra/pr/rebase/BUILD.bazel +++ /dev/null @@ -1,22 +0,0 @@ -load("//dev-infra:defaults.bzl", "ts_library") - -ts_library( - name = "rebase", - srcs = [ - "cli.ts", - "index.ts", - ], - visibility = ["//dev-infra:__subpackages__"], - deps = [ - "//dev-infra/commit-message", - "//dev-infra/utils", - "@npm//@types/conventional-commits-parser", - "@npm//@types/inquirer", - "@npm//@types/node", - "@npm//@types/yargs", - "@npm//conventional-commits-parser", - "@npm//inquirer", - "@npm//typed-graphqlify", - "@npm//yargs", - ], -) diff --git a/dev-infra/pr/rebase/cli.ts b/dev-infra/pr/rebase/cli.ts deleted file mode 100644 index 6b1c0c05f128ed..00000000000000 --- a/dev-infra/pr/rebase/cli.ts +++ /dev/null @@ -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 {Arguments, Argv} from 'yargs'; - -import {addGithubTokenOption} from '../../utils/git/github-yargs'; - -import {rebasePr} from './index'; - -/** The options available to the rebase command via CLI. */ -export interface RebaseCommandOptions { - githubToken: string; - prNumber: number; -} - -/** Builds the rebase pull request command. */ -export function buildRebaseCommand(yargs: Argv): Argv { - return addGithubTokenOption(yargs).positional('prNumber', {type: 'number', demandOption: true}); -} - -/** Handles the rebase pull request command. */ -export async function handleRebaseCommand( - {prNumber, githubToken}: Arguments) { - await rebasePr(prNumber, githubToken); -} diff --git a/dev-infra/pr/rebase/index.ts b/dev-infra/pr/rebase/index.ts deleted file mode 100644 index b9dbd8517f8f8d..00000000000000 --- a/dev-infra/pr/rebase/index.ts +++ /dev/null @@ -1,154 +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 {types as graphqlTypes} from 'typed-graphqlify'; - -import {Commit} from '../../commit-message/parse'; -import {getCommitsInRange} from '../../commit-message/utils'; -import {getConfig, NgDevConfig} from '../../utils/config'; -import {error, info, promptConfirm} from '../../utils/console'; -import {AuthenticatedGitClient} from '../../utils/git/authenticated-git-client'; -import {addTokenToGitHttpsUrl} from '../../utils/git/github-urls'; -import {getPr} from '../../utils/github'; - -/* Graphql schema for the response body for each pending PR. */ -const PR_SCHEMA = { - state: graphqlTypes.string, - maintainerCanModify: graphqlTypes.boolean, - viewerDidAuthor: graphqlTypes.boolean, - headRefOid: graphqlTypes.string, - headRef: { - name: graphqlTypes.string, - repository: { - url: graphqlTypes.string, - nameWithOwner: graphqlTypes.string, - }, - }, - baseRef: { - name: graphqlTypes.string, - repository: { - url: graphqlTypes.string, - nameWithOwner: graphqlTypes.string, - }, - }, -}; - -/** - * Rebase the provided PR onto its merge target branch, and push up the resulting - * commit to the PRs repository. - */ -export async function rebasePr( - prNumber: number, githubToken: string, config: Pick = getConfig()) { - /** The singleton instance of the authenticated git client. */ - const git = AuthenticatedGitClient.get(); - if (git.hasUncommittedChanges()) { - error('Cannot perform rebase of PR with local changes.'); - process.exit(1); - } - - /** - * The branch or revision originally checked out before this method performed - * any Git operations that may change the working branch. - */ - const previousBranchOrRevision = git.getCurrentBranchOrRevision(); - /* Get the PR information from Github. */ - const pr = await getPr(PR_SCHEMA, prNumber, git); - - const headRefName = pr.headRef.name; - const baseRefName = pr.baseRef.name; - const fullHeadRef = `${pr.headRef.repository.nameWithOwner}:${headRefName}`; - const fullBaseRef = `${pr.baseRef.repository.nameWithOwner}:${baseRefName}`; - const headRefUrl = addTokenToGitHttpsUrl(pr.headRef.repository.url, githubToken); - const baseRefUrl = addTokenToGitHttpsUrl(pr.baseRef.repository.url, githubToken); - - // Note: Since we use a detached head for rebasing the PR and therefore do not have - // remote-tracking branches configured, we need to set our expected ref and SHA. This - // allows us to use `--force-with-lease` for the detached head while ensuring that we - // never accidentally override upstream changes that have been pushed in the meanwhile. - // See: - // https://git-scm.com/docs/git-push#Documentation/git-push.txt---force-with-leaseltrefnamegtltexpectgt - const forceWithLeaseFlag = `--force-with-lease=${headRefName}:${pr.headRefOid}`; - - // If the PR does not allow maintainers to modify it, exit as the rebased PR cannot - // be pushed up. - if (!pr.maintainerCanModify && !pr.viewerDidAuthor) { - error( - `Cannot rebase as you did not author the PR and the PR does not allow maintainers` + - `to modify the PR`); - process.exit(1); - } - - try { - // Fetch the branch at the commit of the PR, and check it out in a detached state. - info(`Checking out PR #${prNumber} from ${fullHeadRef}`); - git.run(['fetch', '-q', headRefUrl, headRefName]); - git.run(['checkout', '-q', '--detach', 'FETCH_HEAD']); - // Fetch the PRs target branch and rebase onto it. - info(`Fetching ${fullBaseRef} to rebase #${prNumber} on`); - git.run(['fetch', '-q', baseRefUrl, baseRefName]); - - const commonAncestorSha = git.run(['merge-base', 'HEAD', 'FETCH_HEAD']).stdout.trim(); - - const commits = await getCommitsInRange(commonAncestorSha, 'HEAD'); - - let squashFixups = commits.filter((commit: Commit) => commit.isFixup).length === 0 ? - false : - await promptConfirm( - `PR #${prNumber} contains fixup commits, would you like to squash them during rebase?`, - true); - - info(`Attempting to rebase PR #${prNumber} on ${fullBaseRef}`); - - /** - * Tuple of flags to be added to the rebase command and env object to run the git command. - * - * Additional flags to perform the autosquashing are added when the user confirm squashing of - * fixup commits should occur. - */ - const [flags, env] = squashFixups ? - [['--interactive', '--autosquash'], {...process.env, GIT_SEQUENCE_EDITOR: 'true'}] : - [[], undefined]; - const rebaseResult = git.runGraceful(['rebase', ...flags, 'FETCH_HEAD'], {env: env}); - - // If the rebase was clean, push the rebased PR up to the authors fork. - if (rebaseResult.status === 0) { - info(`Rebase was able to complete automatically without conflicts`); - info(`Pushing rebased PR #${prNumber} to ${fullHeadRef}`); - git.run(['push', headRefUrl, `HEAD:${headRefName}`, forceWithLeaseFlag]); - info(`Rebased and updated PR #${prNumber}`); - git.checkout(previousBranchOrRevision, true); - process.exit(0); - } - } catch (err) { - error(err.message); - git.checkout(previousBranchOrRevision, true); - process.exit(1); - } - - // On automatic rebase failures, prompt to choose if the rebase should be continued - // manually or aborted now. - info(`Rebase was unable to complete automatically without conflicts.`); - // If the command is run in a non-CI environment, prompt to format the files immediately. - const continueRebase = - process.env['CI'] === undefined && await promptConfirm('Manually complete rebase?'); - - if (continueRebase) { - info(`After manually completing rebase, run the following command to update PR #${prNumber}:`); - info(` $ git push ${pr.headRef.repository.url} HEAD:${headRefName} ${forceWithLeaseFlag}`); - info(); - info(`To abort the rebase and return to the state of the repository before this command`); - info(`run the following command:`); - info(` $ git rebase --abort && git reset --hard && git checkout ${previousBranchOrRevision}`); - process.exit(1); - } else { - info(`Cleaning up git state, and restoring previous state.`); - } - - git.checkout(previousBranchOrRevision, true); - process.exit(1); -} diff --git a/dev-infra/pullapprove/BUILD.bazel b/dev-infra/pullapprove/BUILD.bazel deleted file mode 100644 index 7ab68b7d64bb80..00000000000000 --- a/dev-infra/pullapprove/BUILD.bazel +++ /dev/null @@ -1,47 +0,0 @@ -load("//dev-infra:defaults.bzl", "jasmine_node_test", "ts_library") - -ts_library( - name = "pullapprove", - srcs = [ - "cli.ts", - "condition_evaluator.ts", - "group.ts", - "logging.ts", - "parse-yaml.ts", - "pullapprove_arrays.ts", - "utils.ts", - "verify.ts", - ], - visibility = ["//dev-infra:__subpackages__"], - deps = [ - "//dev-infra/utils", - "@npm//@types/minimatch", - "@npm//@types/node", - "@npm//@types/yaml", - "@npm//@types/yargs", - "@npm//minimatch", - "@npm//yaml", - "@npm//yargs", - ], -) - -ts_library( - name = "pullapprove_test_lib", - testonly = True, - srcs = glob( - ["*.spec.ts"], - ), - visibility = ["//visibility:private"], - deps = [ - ":pullapprove", - "@npm//@types/jasmine", - "@npm//@types/node", - "@npm//typescript", - ], -) - -jasmine_node_test( - name = "pullapprove_test", - srcs = [":pullapprove_test_lib"], - visibility = ["//visibility:private"], -) diff --git a/dev-infra/pullapprove/cli.ts b/dev-infra/pullapprove/cli.ts deleted file mode 100644 index f3fa0b083b9db3..00000000000000 --- a/dev-infra/pullapprove/cli.ts +++ /dev/null @@ -1,15 +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 {verify} from './verify'; - -/** Build the parser for the pullapprove commands. */ -export function buildPullapproveParser(localYargs: yargs.Argv) { - return localYargs.help().strict().demandCommand().command( - 'verify', 'Verify the pullapprove config', {}, () => verify()); -} diff --git a/dev-infra/pullapprove/condition_evaluator.ts b/dev-infra/pullapprove/condition_evaluator.ts deleted file mode 100644 index b64eb7a5a8f036..00000000000000 --- a/dev-infra/pullapprove/condition_evaluator.ts +++ /dev/null @@ -1,64 +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 {PullApproveGroup} from './group'; -import {PullApproveGroupArray, PullApproveStringArray} from './pullapprove_arrays'; -import {getOrCreateGlob} from './utils'; - -/** - * Context that is provided to conditions. Conditions can use various helpers - * that PullApprove provides. We try to mock them here. Consult the official - * docs for more details: https://docs.pullapprove.com/config/conditions. - */ -const conditionContext = { - 'len': (value: any[]) => value.length, - 'contains_any_globs': (files: PullApproveStringArray, patterns: string[]) => { - // Note: Do not always create globs for the same pattern again. This method - // could be called for each source file. Creating glob's is expensive. - return files.some(f => patterns.some(pattern => getOrCreateGlob(pattern).match(f))); - }, -}; - -/** - * Converts a given condition to a function that accepts a set of files. The returned - * function can be called to check if the set of files matches the condition. - */ -export function convertConditionToFunction(expr: string): ( - files: string[], groups: PullApproveGroup[]) => boolean { - // Creates a dynamic function with the specified expression. - // The first parameter will be `files` as that corresponds to the supported `files` variable that - // can be accessed in PullApprove condition expressions. The second parameter is the list of - // PullApproveGroups that are accessible in the condition expressions. The followed parameters - // correspond to other context variables provided by PullApprove for conditions. - const evaluateFn = new Function('files', 'groups', ...Object.keys(conditionContext), ` - return (${transformExpressionToJs(expr)}); - `); - - // Create a function that calls the dynamically constructed function which mimics - // the condition expression that is usually evaluated with Python in PullApprove. - return (files, groups) => { - const result = evaluateFn( - new PullApproveStringArray(...files), new PullApproveGroupArray(...groups), - ...Object.values(conditionContext)); - // If an array is returned, we consider the condition as active if the array is not - // empty. This matches PullApprove's condition evaluation that is based on Python. - if (Array.isArray(result)) { - return result.length !== 0; - } - return !!result; - }; -} - -/** - * Transforms a condition expression from PullApprove that is based on python - * so that it can be run inside JavaScript. Current transformations: - * 1. `not <..>` -> `!<..>` - */ -function transformExpressionToJs(expression: string): string { - return expression.replace(/not\s+/g, '!'); -} diff --git a/dev-infra/pullapprove/group.ts b/dev-infra/pullapprove/group.ts deleted file mode 100644 index 2c1b2669065a07..00000000000000 --- a/dev-infra/pullapprove/group.ts +++ /dev/null @@ -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 {error} from '../utils/console'; -import {convertConditionToFunction} from './condition_evaluator'; -import {PullApproveGroupConfig} from './parse-yaml'; -import {PullApproveGroupStateDependencyError} from './pullapprove_arrays'; - -/** A condition for a group. */ -interface GroupCondition { - expression: string; - checkFn: (files: string[], groups: PullApproveGroup[]) => boolean; - matchedFiles: Set; - unverifiable: boolean; -} - -interface GroupReviewers { - users?: string[]; - teams?: string[]; -} - -/** Result of testing files against the group. */ -export interface PullApproveGroupResult { - groupName: string; - matchedConditions: GroupCondition[]; - matchedCount: number; - unmatchedConditions: GroupCondition[]; - unmatchedCount: number; - unverifiableConditions: GroupCondition[]; -} - -// Regular expression that matches conditions for the global approval. -const GLOBAL_APPROVAL_CONDITION_REGEX = /^"global-(docs-)?approvers" not in groups.approved$/; - -/** A PullApprove group to be able to test files against. */ -export class PullApproveGroup { - /** List of conditions for the group. */ - readonly conditions: GroupCondition[] = []; - /** List of reviewers for the group. */ - readonly reviewers: GroupReviewers; - - constructor( - public groupName: string, config: PullApproveGroupConfig, - readonly precedingGroups: PullApproveGroup[] = []) { - this._captureConditions(config); - this.reviewers = config.reviewers ?? {users: [], teams: []}; - } - - private _captureConditions(config: PullApproveGroupConfig) { - if (config.conditions) { - return config.conditions.forEach(condition => { - const expression = condition.trim(); - - if (expression.match(GLOBAL_APPROVAL_CONDITION_REGEX)) { - // Currently a noop as we don't take any action for global approval conditions. - return; - } - - try { - this.conditions.push({ - expression, - checkFn: convertConditionToFunction(expression), - matchedFiles: new Set(), - unverifiable: false, - }); - } catch (e) { - error(`Could not parse condition in group: ${this.groupName}`); - error(` - ${expression}`); - error(`Error:`); - error(e.message); - error(e.stack); - process.exit(1); - } - }); - } - } - - /** - * Tests a provided file path to determine if it would be considered matched by - * the pull approve group's conditions. - */ - testFile(filePath: string): boolean { - return this.conditions.every((condition) => { - const {matchedFiles, checkFn, expression} = condition; - try { - const matchesFile = checkFn([filePath], this.precedingGroups); - if (matchesFile) { - matchedFiles.add(filePath); - } - return matchesFile; - } catch (e) { - // In the case of a condition that depends on the state of groups we want to - // ignore that the verification can't accurately evaluate the condition and then - // continue processing. Other types of errors fail the verification, as conditions - // should otherwise be able to execute without throwing. - if (e instanceof PullApproveGroupStateDependencyError) { - condition.unverifiable = true; - // Return true so that `this.conditions.every` can continue evaluating. - return true; - } else { - const errMessage = `Condition could not be evaluated: \n\n` + - `From the [${this.groupName}] group:\n` + - ` - ${expression}` + - `\n\n${e.message} ${e.stack}\n\n`; - error(errMessage); - process.exit(1); - } - } - }); - } - - /** Retrieve the results for the Group, all matched and unmatched conditions. */ - getResults(): PullApproveGroupResult { - const matchedConditions = this.conditions.filter(c => c.matchedFiles.size > 0); - const unmatchedConditions = - this.conditions.filter(c => c.matchedFiles.size === 0 && !c.unverifiable); - const unverifiableConditions = this.conditions.filter(c => c.unverifiable); - return { - matchedConditions, - matchedCount: matchedConditions.length, - unmatchedConditions, - unmatchedCount: unmatchedConditions.length, - unverifiableConditions, - groupName: this.groupName, - }; - } -} diff --git a/dev-infra/pullapprove/logging.ts b/dev-infra/pullapprove/logging.ts deleted file mode 100644 index 573516e23e7388..00000000000000 --- a/dev-infra/pullapprove/logging.ts +++ /dev/null @@ -1,46 +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 {info} from '../utils/console'; -import {PullApproveGroupResult} from './group'; - -type ConditionGrouping = keyof Pick< - PullApproveGroupResult, 'matchedConditions'|'unmatchedConditions'|'unverifiableConditions'>; - -/** Create logs for each pullapprove group result. */ -export function logGroup( - group: PullApproveGroupResult, conditionsToPrint: ConditionGrouping, printMessageFn = info) { - const conditions = group[conditionsToPrint]; - printMessageFn.group(`[${group.groupName}]`); - if (conditions.length) { - conditions.forEach(groupCondition => { - const count = groupCondition.matchedFiles.size; - if (conditionsToPrint === 'unverifiableConditions') { - printMessageFn(`${groupCondition.expression}`); - } else { - printMessageFn( - `${count} ${count === 1 ? 'match' : 'matches'} - ${groupCondition.expression}`); - } - }); - printMessageFn.groupEnd(); - } -} - -/** Logs a header within a text drawn box. */ -export function logHeader(...params: string[]) { - const totalWidth = 80; - const fillWidth = totalWidth - 2; - const headerText = params.join(' ').substr(0, fillWidth); - const leftSpace = Math.ceil((fillWidth - headerText.length) / 2); - const rightSpace = fillWidth - leftSpace - headerText.length; - const fill = (count: number, content: string) => content.repeat(count); - - info(`┌${fill(fillWidth, '─')}┐`); - info(`│${fill(leftSpace, ' ')}${headerText}${fill(rightSpace, ' ')}│`); - info(`└${fill(fillWidth, '─')}┘`); -} diff --git a/dev-infra/pullapprove/parse-yaml.ts b/dev-infra/pullapprove/parse-yaml.ts deleted file mode 100644 index fc0289ecce7efd..00000000000000 --- a/dev-infra/pullapprove/parse-yaml.ts +++ /dev/null @@ -1,45 +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 {parse as parseYaml} from 'yaml'; -import {PullApproveGroup} from './group'; - -export interface PullApproveGroupConfig { - conditions?: string[]; - reviewers?: { - users: string[], - teams?: string[], - }|{ - teams: string[], - }; -} - -export interface PullApproveConfig { - version: number; - github_api_version?: string; - pullapprove_conditions?: { - condition: string, - unmet_status: string, - explanation: string, - }[]; - groups: { - [key: string]: PullApproveGroupConfig, - }; -} - -export function parsePullApproveYaml(rawYaml: string): PullApproveConfig { - return parseYaml(rawYaml, {merge: true}) as PullApproveConfig; -} - -/** Parses all of the groups defined in the pullapprove yaml. */ -export function getGroupsFromYaml(pullApproveYamlRaw: string): PullApproveGroup[] { - /** JSON representation of the pullapprove yaml file. */ - const pullApprove = parsePullApproveYaml(pullApproveYamlRaw); - return Object.entries(pullApprove.groups).reduce((groups, [groupName, group]) => { - return groups.concat(new PullApproveGroup(groupName, group, groups)); - }, [] as PullApproveGroup[]); -} diff --git a/dev-infra/pullapprove/pullapprove_arrays.ts b/dev-infra/pullapprove/pullapprove_arrays.ts deleted file mode 100644 index 46cec6d5227bae..00000000000000 --- a/dev-infra/pullapprove/pullapprove_arrays.ts +++ /dev/null @@ -1,89 +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 {PullApproveGroup} from './group'; -import {getOrCreateGlob} from './utils'; - -export class PullApproveGroupStateDependencyError extends Error { - constructor(message?: string) { - super(message); - // 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, PullApproveGroupStateDependencyError.prototype); - // Error names are displayed in their stack but can't be set in the constructor. - this.name = PullApproveGroupStateDependencyError.name; - } -} - -/** - * Superset of a native array. The superset provides methods which mimic the - * list data structure used in PullApprove for files in conditions. - */ -export class PullApproveStringArray extends Array { - constructor(...elements: string[]) { - super(...elements); - - // 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, PullApproveStringArray.prototype); - } - /** Returns a new array which only includes files that match the given pattern. */ - include(pattern: string): PullApproveStringArray { - return new PullApproveStringArray(...this.filter(s => getOrCreateGlob(pattern).match(s))); - } - - /** Returns a new array which only includes files that did not match the given pattern. */ - exclude(pattern: string): PullApproveStringArray { - return new PullApproveStringArray(...this.filter(s => !getOrCreateGlob(pattern).match(s))); - } -} - -/** - * Superset of a native array. The superset provides methods which mimic the - * list data structure used in PullApprove for groups in conditions. - */ -export class PullApproveGroupArray extends Array { - constructor(...elements: PullApproveGroup[]) { - super(...elements); - - // 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, PullApproveGroupArray.prototype); - } - - include(pattern: string): PullApproveGroupArray { - return new PullApproveGroupArray(...this.filter(s => s.groupName.match(pattern))); - } - - /** Returns a new array which only includes files that did not match the given pattern. */ - exclude(pattern: string): PullApproveGroupArray { - return new PullApproveGroupArray(...this.filter(s => s.groupName.match(pattern))); - } - - get pending() { - throw new PullApproveGroupStateDependencyError(); - } - - get active() { - throw new PullApproveGroupStateDependencyError(); - } - - get inactive() { - throw new PullApproveGroupStateDependencyError(); - } - - get rejected() { - throw new PullApproveGroupStateDependencyError(); - } - - get names() { - return this.map(g => g.groupName); - } -} diff --git a/dev-infra/pullapprove/utils.ts b/dev-infra/pullapprove/utils.ts deleted file mode 100644 index 8e93f07a9f7f1b..00000000000000 --- a/dev-infra/pullapprove/utils.ts +++ /dev/null @@ -1,24 +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 {IMinimatch, Minimatch} from 'minimatch'; - -/** Map that holds patterns and their corresponding Minimatch globs. */ -const patternCache = new Map(); - -/** - * Gets a glob for the given pattern. The cached glob will be returned - * if available. Otherwise a new glob will be created and cached. - */ -export function getOrCreateGlob(pattern: string) { - if (patternCache.has(pattern)) { - return patternCache.get(pattern)!; - } - const glob = new Minimatch(pattern, {dot: true}); - patternCache.set(pattern, glob); - return glob; -} diff --git a/dev-infra/pullapprove/verify.spec.ts b/dev-infra/pullapprove/verify.spec.ts deleted file mode 100644 index f3ddb620887e78..00000000000000 --- a/dev-infra/pullapprove/verify.spec.ts +++ /dev/null @@ -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 {PullApproveGroup} from './group'; -import {getGroupsFromYaml} from './parse-yaml'; - -describe('group parsing', () => { - it('gets group name', () => { - const groupName = 'fw-migrations'; - const groups = getGroupsFromYaml(` - groups: - ${groupName}: - type: optional - `); - expect(groups[0].groupName).toBe(groupName); - }); - - it('gets correct number of groups', () => { - const groups = getGroupsFromYaml(` - groups: - fw-migrations: - type: optional - fw-core: - type: optional - `); - expect(groups.length).toBe(2); - }); - - it('gets preceding groups', () => { - const groups = getGroupsFromYaml(` - groups: - fw-migrations: - type: optional - fw-core: - type: optional - dev-infra: - type: optional - `); - const fwMigrations = getGroupByName(groups, 'fw-migrations')!; - const fwCore = getGroupByName(groups, 'fw-core')!; - const devInfra = getGroupByName(groups, 'dev-infra')!; - expect(getGroupNames(fwMigrations.precedingGroups)).toEqual([]); - expect(getGroupNames(fwCore.precedingGroups)).toEqual([fwMigrations.groupName]); - expect(getGroupNames(devInfra.precedingGroups)).toEqual([ - fwMigrations.groupName, fwCore.groupName - ]); - }); - - it('matches file conditions', () => { - const groups = getGroupsFromYaml(` - groups: - fw-core: - conditions: - - contains_any_globs(files, ['packages/core/**']) - `); - const fwCore = getGroupByName(groups, 'fw-core')!; - expect(fwCore.testFile('packages/core/test.ts')).toBe(true); - expect(fwCore.testFile('some/other/location/test.ts')).toBe(false); - }); - - it('allows conditions based on groups', () => { - const groups = getGroupsFromYaml(` - groups: - fw-migrations: - conditions: - - len(groups) > 0 - fw-core: - conditions: - - len(groups.active) > 0 - `); - const fwMigrations = getGroupByName(groups, 'fw-migrations')!; - expect(() => fwMigrations.testFile('any')).not.toThrow(); - const fwCore = getGroupByName(groups, 'fw-core')!; - expect(() => fwCore.testFile('any')).not.toThrow(); - }); -}); - -function getGroupByName(groups: PullApproveGroup[], name: string): PullApproveGroup|undefined { - return groups.find(g => g.groupName === name); -} - -function getGroupNames(groups: PullApproveGroup[]) { - return groups.map(g => g.groupName); -} diff --git a/dev-infra/pullapprove/verify.ts b/dev-infra/pullapprove/verify.ts deleted file mode 100644 index 93bfe9948ec2c1..00000000000000 --- a/dev-infra/pullapprove/verify.ts +++ /dev/null @@ -1,114 +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 {debug, info} from '../utils/console'; -import {GitClient} from '../utils/git/git-client'; -import {logGroup, logHeader} from './logging'; -import {getGroupsFromYaml} from './parse-yaml'; - -export function verify() { - const git = GitClient.get(); - /** Full path to PullApprove config file */ - const PULL_APPROVE_YAML_PATH = resolve(git.baseDir, '.pullapprove.yml'); - /** All tracked files in the repository. */ - const REPO_FILES = git.allFiles(); - /** The pull approve config file. */ - const pullApproveYamlRaw = readFileSync(PULL_APPROVE_YAML_PATH, 'utf8'); - /** All of the groups defined in the pullapprove yaml. */ - const groups = getGroupsFromYaml(pullApproveYamlRaw); - /** - * PullApprove groups without conditions. These are skipped in the verification - * as those would always be active and cause zero unmatched files. - */ - const groupsSkipped = groups.filter(group => !group.conditions.length); - /** PullApprove groups with conditions. */ - const groupsWithConditions = groups.filter(group => !!group.conditions.length); - /** Files which are matched by at least one group. */ - const matchedFiles: string[] = []; - /** Files which are not matched by at least one group. */ - const unmatchedFiles: string[] = []; - - // Test each file in the repo against each group for being matched. - REPO_FILES.forEach((file: string) => { - if (groupsWithConditions.filter(group => group.testFile(file)).length) { - matchedFiles.push(file); - } else { - unmatchedFiles.push(file); - } - }); - /** Results for each group */ - const resultsByGroup = groupsWithConditions.map(group => group.getResults()); - /** - * Whether all group condition lines match at least one file and all files - * are matched by at least one group. - */ - const allGroupConditionsValid = - resultsByGroup.every(r => !r.unmatchedCount) && !unmatchedFiles.length; - /** Whether all groups have at least one reviewer user or team defined. */ - const groupsWithoutReviewers = groups.filter(group => Object.keys(group.reviewers).length === 0); - /** The overall result of the verifcation. */ - const overallResult = allGroupConditionsValid && groupsWithoutReviewers.length === 0; - - /** - * Overall result - */ - logHeader('Overall Result'); - if (overallResult) { - info('PullApprove verification succeeded!'); - } else { - info(`PullApprove verification failed.`); - info(); - info(`Please update '.pullapprove.yml' to ensure that all necessary`); - info(`files/directories have owners and all patterns that appear in`); - info(`the file correspond to actual files/directories in the repo.`); - } - /** Reviewers check */ - logHeader(`Group Reviewers Check`); - if (groupsWithoutReviewers.length === 0) { - info('All group contain at least one reviewer user or team.'); - } else { - info.group(`Discovered ${groupsWithoutReviewers.length} group(s) without a reviewer defined`); - groupsWithoutReviewers.forEach(g => info(g.groupName)); - info.groupEnd(); - } - /** - * File by file Summary - */ - logHeader('PullApprove results by file'); - info.group(`Matched Files (${matchedFiles.length} files)`); - matchedFiles.forEach(file => debug(file)); - info.groupEnd(); - info.group(`Unmatched Files (${unmatchedFiles.length} files)`); - unmatchedFiles.forEach(file => info(file)); - info.groupEnd(); - /** - * Group by group Summary - */ - logHeader('PullApprove results by group'); - info.group(`Groups skipped (${groupsSkipped.length} groups)`); - groupsSkipped.forEach(group => debug(`${group.groupName}`)); - info.groupEnd(); - const matchedGroups = resultsByGroup.filter(group => !group.unmatchedCount); - info.group(`Matched conditions by Group (${matchedGroups.length} groups)`); - matchedGroups.forEach(group => logGroup(group, 'matchedConditions', debug)); - info.groupEnd(); - const unmatchedGroups = resultsByGroup.filter(group => group.unmatchedCount); - info.group(`Unmatched conditions by Group (${unmatchedGroups.length} groups)`); - unmatchedGroups.forEach(group => logGroup(group, 'unmatchedConditions')); - info.groupEnd(); - const unverifiableConditionsInGroups = - resultsByGroup.filter(group => group.unverifiableConditions.length > 0); - info.group(`Unverifiable conditions by Group (${unverifiableConditionsInGroups.length} groups)`); - unverifiableConditionsInGroups.forEach(group => logGroup(group, 'unverifiableConditions')); - info.groupEnd(); - - // Provide correct exit code based on verification success. - process.exit(overallResult ? 0 : 1); -} diff --git a/dev-infra/release/BUILD.bazel b/dev-infra/release/BUILD.bazel deleted file mode 100644 index 54b73c76a12b89..00000000000000 --- a/dev-infra/release/BUILD.bazel +++ /dev/null @@ -1,22 +0,0 @@ -load("//dev-infra:defaults.bzl", "ts_library") - -ts_library( - name = "release", - srcs = glob([ - "**/*.ts", - ]), - visibility = ["//dev-infra:__subpackages__"], - deps = [ - "//dev-infra/release/build", - "//dev-infra/release/info", - "//dev-infra/release/notes", - "//dev-infra/release/publish", - "//dev-infra/release/set-dist-tag", - "//dev-infra/utils", - "@npm//@types/node", - "@npm//@types/semver", - "@npm//@types/yargs", - "@npm//semver", - "@npm//yargs", - ], -) diff --git a/dev-infra/release/build/BUILD.bazel b/dev-infra/release/build/BUILD.bazel deleted file mode 100644 index de0198f8b45903..00000000000000 --- a/dev-infra/release/build/BUILD.bazel +++ /dev/null @@ -1,40 +0,0 @@ -load("//dev-infra:defaults.bzl", "jasmine_node_test", "ts_library") - -exports_files([ - "build-worker.ts", -]) - -ts_library( - name = "build", - srcs = glob( - [ - "**/*.ts", - ], - exclude = ["*.spec.ts"], - ), - visibility = ["//dev-infra:__subpackages__"], - deps = [ - "//dev-infra/release/config", - "//dev-infra/utils", - "@npm//@types/node", - "@npm//@types/yargs", - ], -) - -ts_library( - name = "test_lib", - srcs = glob([ - "*.spec.ts", - ]), - deps = [ - ":build", - "//dev-infra/release/config", - "@npm//@types/jasmine", - "@npm//@types/node", - ], -) - -jasmine_node_test( - name = "test", - deps = [":test_lib"], -) diff --git a/dev-infra/release/build/build-worker.ts b/dev-infra/release/build/build-worker.ts deleted file mode 100644 index 97d7a9bcecb579..00000000000000 --- a/dev-infra/release/build/build-worker.ts +++ /dev/null @@ -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 - */ - -/* - * This file will be spawned as a separate process when the `ng-dev release build` command is - * invoked. A separate process allows us to hide any superfluous stdout output from arbitrary - * build commands that we cannot control. This is necessary as the `ng-dev release build` command - * supports stdout JSON output that should be parsable and not polluted from other stdout messages. - */ - -import {getReleaseConfig} from '../config/index'; - -// Start the release package building. -main(process.argv[2] === 'true'); - -/** Main function for building the release packages. */ -async function main(stampForRelease: boolean) { - if (process.send === undefined) { - throw Error('This script needs to be invoked as a NodeJS worker.'); - } - - const config = getReleaseConfig(); - const builtPackages = await config.buildPackages(stampForRelease); - - // Transfer the built packages back to the parent process. - process.send(builtPackages); -} diff --git a/dev-infra/release/build/build.spec.ts b/dev-infra/release/build/build.spec.ts deleted file mode 100644 index 1853a665386b06..00000000000000 --- a/dev-infra/release/build/build.spec.ts +++ /dev/null @@ -1,77 +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 releaseConfig from '../config/index'; -import {ReleaseBuildCommandModule} from './cli'; -import * as index from './index'; - -describe('ng-dev release build', () => { - let npmPackages: string[]; - let buildPackages: jasmine.Spy; - - beforeEach(() => { - npmPackages = ['@angular/pkg1', '@angular/pkg2']; - buildPackages = jasmine.createSpy('buildPackages').and.resolveTo([ - {name: '@angular/pkg1', outputPath: 'dist/pkg1'}, - {name: '@angular/pkg2', outputPath: 'dist/pkg2'}, - ]); - - // We cannot test the worker process, so we fake the worker function and - // directly call the package build function. - spyOn(index, 'buildReleaseOutput').and.callFake(() => buildPackages()); - // We need to stub out the `process.exit` function during tests as the CLI - // handler calls those in case of failures. - spyOn(process, 'exit'); - }); - - /** Invokes the build command handler. */ - async function invokeBuild({json}: {json?: boolean} = {}) { - spyOn(releaseConfig, 'getReleaseConfig') - .and.returnValue({npmPackages, buildPackages, releaseNotes: {}}); - await ReleaseBuildCommandModule.handler({json: !!json, stampForRelease: true, $0: '', _: []}); - } - - it('should invoke configured build packages function', async () => { - await invokeBuild(); - expect(buildPackages).toHaveBeenCalledTimes(1); - expect(process.exit).toHaveBeenCalledTimes(0); - }); - - it('should print built packages as JSON if `--json` is specified', async () => { - const writeSpy = spyOn(process.stdout, 'write'); - await invokeBuild({json: true}); - - expect(buildPackages).toHaveBeenCalledTimes(1); - expect(writeSpy).toHaveBeenCalledTimes(1); - - const jsonText = writeSpy.calls.mostRecent().args[0] as string; - const parsed = JSON.parse(jsonText) as releaseConfig.BuiltPackage[]; - - expect(parsed).toEqual([ - {name: '@angular/pkg1', outputPath: 'dist/pkg1'}, - {name: '@angular/pkg2', outputPath: 'dist/pkg2'} - ]); - expect(process.exit).toHaveBeenCalledTimes(0); - }); - - it('should error if package has not been built', async () => { - // Set up an NPM package that is not built. - npmPackages.push('@angular/non-existent'); - - spyOn(console, 'error'); - await invokeBuild(); - - expect(console.error).toHaveBeenCalledTimes(2); - expect(console.error) - .toHaveBeenCalledWith( - jasmine.stringMatching(`Release output missing for the following packages`)); - expect(console.error).toHaveBeenCalledWith(jasmine.stringMatching(`- @angular/non-existent`)); - expect(process.exit).toHaveBeenCalledTimes(1); - expect(process.exit).toHaveBeenCalledWith(1); - }); -}); diff --git a/dev-infra/release/build/cli.ts b/dev-infra/release/build/cli.ts deleted file mode 100644 index 6125dad1a9eb34..00000000000000 --- a/dev-infra/release/build/cli.ts +++ /dev/null @@ -1,75 +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 {getConfig} from '../../utils/config'; -import {error, green, info, red, warn, yellow} from '../../utils/console'; -import {BuiltPackage, getReleaseConfig} from '../config/index'; - -import {buildReleaseOutput} from './index'; - -/** Command line options for building a release. */ -export interface ReleaseBuildOptions { - json: boolean; -} - -/** Yargs command builder for configuring the `ng-dev release build` command. */ -function builder(argv: Argv): Argv { - return argv.option('json', { - type: 'boolean', - description: 'Whether the built packages should be printed to stdout as JSON.', - default: false, - }); -} - -/** Yargs command handler for building a release. */ -async function handler(args: Arguments) { - const {npmPackages} = getReleaseConfig(); - let builtPackages = await buildReleaseOutput(true); - - // If package building failed, print an error and exit with an error code. - if (builtPackages === null) { - error(red(` ✘ Could not build release output. Please check output above.`)); - process.exit(1); - } - - // If no packages have been built, we assume that this is never correct - // and exit with an error code. - if (builtPackages.length === 0) { - error(red(` ✘ No release packages have been built. Please ensure that the`)); - error(red(` build script is configured correctly in ".ng-dev".`)); - process.exit(1); - } - - const missingPackages = - npmPackages.filter(pkgName => !builtPackages!.find(b => b.name === pkgName)); - - // Check for configured release packages which have not been built. We want to - // error and exit if any configured package has not been built. - if (missingPackages.length > 0) { - error(red(` ✘ Release output missing for the following packages:`)); - missingPackages.forEach(pkgName => error(red(` - ${pkgName}`))); - process.exit(1); - } - - if (args.json) { - process.stdout.write(JSON.stringify(builtPackages, null, 2)); - } else { - info(green(' ✓ Built release packages.')); - builtPackages.forEach(({name}) => info(green(` - ${name}`))); - } -} - -/** CLI command module for building release output. */ -export const ReleaseBuildCommandModule: CommandModule<{}, ReleaseBuildOptions> = { - builder, - handler, - command: 'build', - describe: 'Builds the release output for the current branch.', -}; diff --git a/dev-infra/release/build/index.ts b/dev-infra/release/build/index.ts deleted file mode 100644 index 97bf1c38475e24..00000000000000 --- a/dev-infra/release/build/index.ts +++ /dev/null @@ -1,37 +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 {fork} from 'child_process'; -import {BuiltPackage} from '../config/index'; - -/** - * Builds the release output without polluting the process stdout. Build scripts commonly - * print messages to stderr or stdout. This is fine in most cases, but sometimes other tooling - * reserves stdout for data transfer (e.g. when `ng release build --json` is invoked). To not - * pollute the stdout in such cases, we launch a child process for building the release packages - * and redirect all stdout output to the stderr channel (which can be read in the terminal). - */ -export async function buildReleaseOutput(stampForRelease: boolean = false): - Promise { - return new Promise(resolve => { - const buildProcess = fork(require.resolve('./build-worker'), [`${stampForRelease}`], { - // The stdio option is set to redirect any "stdout" output directly to the "stderr" file - // descriptor. An additional "ipc" file descriptor is created to support communication with - // the build process. https://nodejs.org/api/child_process.html#child_process_options_stdio. - stdio: ['inherit', 2, 2, 'ipc'], - }); - let builtPackages: BuiltPackage[]|null = null; - - // The child process will pass the `buildPackages()` output through the - // IPC channel. We keep track of it so that we can use it as resolve value. - buildProcess.on('message', buildResponse => builtPackages = buildResponse); - - // On child process exit, resolve the promise with the received output. - buildProcess.on('exit', () => resolve(builtPackages)); - }); -} diff --git a/dev-infra/release/cli.ts b/dev-infra/release/cli.ts deleted file mode 100644 index 7bcffa03123147..00000000000000 --- a/dev-infra/release/cli.ts +++ /dev/null @@ -1,28 +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 {ReleaseBuildCommandModule} from './build/cli'; -import {ReleaseInfoCommandModule} from './info/cli'; -import {ReleaseNotesCommandModule} from './notes/cli'; -import {ReleasePublishCommandModule} from './publish/cli'; -import {ReleaseSetDistTagCommand} from './set-dist-tag/cli'; -import {BuildEnvStampCommand} from './stamping/cli'; - -/** Build the parser for the release commands. */ -export function buildReleaseParser(localYargs: yargs.Argv) { - return localYargs.help() - .strict() - .demandCommand() - .command(ReleasePublishCommandModule) - .command(ReleaseBuildCommandModule) - .command(ReleaseInfoCommandModule) - .command(ReleaseSetDistTagCommand) - .command(BuildEnvStampCommand) - .command(ReleaseNotesCommandModule); -} diff --git a/dev-infra/release/config/BUILD.bazel b/dev-infra/release/config/BUILD.bazel deleted file mode 100644 index c8170c2acab4a4..00000000000000 --- a/dev-infra/release/config/BUILD.bazel +++ /dev/null @@ -1,13 +0,0 @@ -load("//dev-infra:defaults.bzl", "ts_library") - -ts_library( - name = "config", - srcs = glob([ - "**/*.ts", - ]), - visibility = ["//dev-infra:__subpackages__"], - deps = [ - "//dev-infra/utils", - "@npm//@types/semver", - ], -) diff --git a/dev-infra/release/config/index.ts b/dev-infra/release/config/index.ts deleted file mode 100644 index 2f4bd12e46acbe..00000000000000 --- a/dev-infra/release/config/index.ts +++ /dev/null @@ -1,72 +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 describing a built package. */ -export interface BuiltPackage { - /** Name of the package. */ - name: string; - /** Path to the package output directory. */ - outputPath: string; -} - -/** Configuration for staging and publishing a release. */ -export interface ReleaseConfig { - /** Registry URL used for publishing release packages. Defaults to the NPM registry. */ - publishRegistry?: string; - /** List of NPM packages that are published as part of this project. */ - npmPackages: string[]; - /** Builds release packages and returns a list of paths pointing to the output. */ - buildPackages: (stampForRelease?: boolean) => Promise; - /** The list of github labels to add to the release PRs. */ - releasePrLabels?: string[]; - /** Configuration for creating release notes during publishing. */ - releaseNotes: ReleaseNotesConfig; -} - -/** Configuration for creating release notes during publishing. */ -export interface ReleaseNotesConfig { - /** Whether to prompt for and include a release title in the generated release notes. */ - useReleaseTitle?: boolean; - /** List of commit scopes to disclude from generated release notes. */ - hiddenScopes?: string[]; - /** - * List of commit groups, either {npmScope}/{scope} or {scope}, to use for ordering. - * - * Each group for the release notes, will appear in the order provided in groupOrder and any other - * groups will appear after these groups, sorted by `Array.sort`'s default sorting order. - */ - groupOrder?: string[]; -} - -/** Configuration for releases in the dev-infra configuration. */ -export type DevInfraReleaseConfig = NgDevConfig<{release: ReleaseConfig}>; - -/** Retrieve and validate the config as `ReleaseConfig`. */ -export function getReleaseConfig(config: Partial = getConfig()): - ReleaseConfig { - // List of errors encountered validating the config. - const errors: string[] = []; - - if (config.release === undefined) { - errors.push(`No configuration defined for "release"`); - } - if (config.release?.npmPackages === undefined) { - errors.push(`No "npmPackages" configured for releasing.`); - } - if (config.release?.buildPackages === undefined) { - errors.push(`No "buildPackages" function configured for releasing.`); - } - if (config.release?.releaseNotes === undefined) { - errors.push(`No "releaseNotes" configured for releasing.`); - } - - assertNoErrors(errors); - return config.release!; -} diff --git a/dev-infra/release/info/BUILD.bazel b/dev-infra/release/info/BUILD.bazel deleted file mode 100644 index 62b6cdf6fa4887..00000000000000 --- a/dev-infra/release/info/BUILD.bazel +++ /dev/null @@ -1,14 +0,0 @@ -load("//dev-infra:defaults.bzl", "ts_library") - -ts_library( - name = "info", - srcs = ["cli.ts"], - visibility = ["//dev-infra:__subpackages__"], - deps = [ - "//dev-infra/release/config", - "//dev-infra/release/versioning", - "//dev-infra/utils", - "@npm//@types/node", - "@npm//@types/yargs", - ], -) diff --git a/dev-infra/release/info/cli.ts b/dev-infra/release/info/cli.ts deleted file mode 100644 index 966f4d0c25f0ae..00000000000000 --- a/dev-infra/release/info/cli.ts +++ /dev/null @@ -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 {CommandModule} from 'yargs'; - -import {info} from '../../utils/console'; -import {GitClient} from '../../utils/git/git-client'; -import {getReleaseConfig} from '../config/index'; -import {fetchActiveReleaseTrains} from '../versioning/active-release-trains'; -import {printActiveReleaseTrains} from '../versioning/print-active-trains'; - -/** Yargs command handler for printing release information. */ -async function handler() { - const git = GitClient.get(); - const gitRepoWithApi = {api: git.github, ...git.remoteConfig}; - const releaseTrains = await fetchActiveReleaseTrains(gitRepoWithApi); - - // Print the active release trains. - await printActiveReleaseTrains(releaseTrains, getReleaseConfig()); -} - -/** CLI command module for retrieving release information. */ -export const ReleaseInfoCommandModule: CommandModule = { - handler, - command: 'info', - describe: 'Prints active release trains to the console.', -}; diff --git a/dev-infra/release/notes/BUILD.bazel b/dev-infra/release/notes/BUILD.bazel deleted file mode 100644 index 7fabae8cc6bef1..00000000000000 --- a/dev-infra/release/notes/BUILD.bazel +++ /dev/null @@ -1,21 +0,0 @@ -load("//dev-infra:defaults.bzl", "ts_library") - -ts_library( - name = "notes", - srcs = glob([ - "**/*.ts", - ]), - visibility = ["//dev-infra:__subpackages__"], - deps = [ - "//dev-infra/commit-message", - "//dev-infra/release/config", - "//dev-infra/release/versioning", - "//dev-infra/utils", - "@npm//@types/ejs", - "@npm//@types/node", - "@npm//@types/semver", - "@npm//@types/yargs", - "@npm//ejs", - "@npm//semver", - ], -) diff --git a/dev-infra/release/notes/cli.ts b/dev-infra/release/notes/cli.ts deleted file mode 100644 index 5f6e5217abfbe2..00000000000000 --- a/dev-infra/release/notes/cli.ts +++ /dev/null @@ -1,84 +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 {join} from 'path'; -import {SemVer} from 'semver'; -import {Arguments, Argv, CommandModule} from 'yargs'; - -import {info} from '../../utils/console'; -import {GitClient} from '../../utils/git/git-client'; - -import {ReleaseNotes} from './release-notes'; - -/** Command line options for building a release. */ -export interface ReleaseNotesOptions { - from?: string; - to: string; - outFile?: string; - releaseVersion: SemVer; - type: 'github-release'|'changelog'; -} - -/** Yargs command builder for configuring the `ng-dev release build` command. */ -function builder(argv: Argv): Argv { - return argv - .option( - 'releaseVersion', - {type: 'string', default: '0.0.0', coerce: (version: string) => new SemVer(version)}) - .option('from', { - type: 'string', - description: 'The git tag or ref to start the changelog entry from', - defaultDescription: 'The latest semver tag', - }) - .option('to', { - type: 'string', - description: 'The git tag or ref to end the changelog entry with', - default: 'HEAD', - }) - .option('type', { - type: 'string', - description: 'The type of release notes to create', - choices: ['github-release', 'changelog'] as const, - default: 'changelog' as const, - }) - .option('outFile', { - type: 'string', - description: 'File location to write the generated release notes to', - coerce: (filePath?: string) => filePath ? join(process.cwd(), filePath) : undefined - }); -} - -/** Yargs command handler for generating release notes. */ -async function handler({releaseVersion, from, to, outFile, type}: Arguments) { - // Since `yargs` evaluates defaults even if a value as been provided, if no value is provided to - // the handler, the latest semver tag on the branch is used. - from = from || GitClient.get().getLatestSemverTag().format(); - /** The ReleaseNotes instance to generate release notes. */ - const releaseNotes = await ReleaseNotes.fromRange(releaseVersion, from, to); - - /** The requested release notes entry. */ - const releaseNotesEntry = await ( - type === 'changelog' ? releaseNotes.getChangelogEntry() : - releaseNotes.getGithubReleaseEntry()); - - if (outFile) { - writeFileSync(outFile, releaseNotesEntry); - info(`Generated release notes for "${releaseVersion}" written to ${outFile}`); - } else { - process.stdout.write(releaseNotesEntry); - } -} - -/** CLI command module for generating release notes. */ -export const ReleaseNotesCommandModule: CommandModule<{}, ReleaseNotesOptions> = { - builder, - handler, - command: 'notes', - describe: 'Generate release notes', -}; diff --git a/dev-infra/release/notes/context.ts b/dev-infra/release/notes/context.ts deleted file mode 100644 index c1c4093904cfb3..00000000000000 --- a/dev-infra/release/notes/context.ts +++ /dev/null @@ -1,178 +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_TYPES, ReleaseNotesLevel} from '../../commit-message/config'; -import {CommitFromGitLog} from '../../commit-message/parse'; -import {GithubConfig} from '../../utils/config'; -import {ReleaseNotesConfig} from '../config/index'; - - -/** List of types to be included in the release notes. */ -const typesToIncludeInReleaseNotes = - Object.values(COMMIT_TYPES) - .filter(type => type.releaseNotesLevel === ReleaseNotesLevel.Visible) - .map(type => type.name); - -/** Data used for context during rendering. */ -export interface RenderContextData { - title: string|false; - groupOrder?: ReleaseNotesConfig['groupOrder']; - hiddenScopes?: ReleaseNotesConfig['hiddenScopes']; - date?: Date; - commits: CommitFromGitLog[]; - version: string; - github: GithubConfig; -} - -/** Context class used for rendering release notes. */ -export class RenderContext { - /** An array of group names in sort order if defined. */ - private readonly groupOrder = this.data.groupOrder || []; - /** An array of scopes to hide from the release entry output. */ - private readonly hiddenScopes = this.data.hiddenScopes || []; - /** The title of the release, or `false` if no title should be used. */ - readonly title = this.data.title; - /** An array of commits in the release period. */ - readonly commits = this.data.commits; - /** The version of the release. */ - readonly version = this.data.version; - /** The date stamp string for use in the release notes entry. */ - readonly dateStamp = buildDateStamp(this.data.date); - - constructor(private readonly data: RenderContextData) {} - - /** - * Organizes and sorts the commits into groups of commits. - * - * Groups are sorted either by default `Array.sort` order, or using the provided group order from - * the configuration. Commits are order in the same order within each groups commit list as they - * appear in the provided list of commits. - * */ - asCommitGroups(commits: CommitFromGitLog[]) { - /** The discovered groups to organize into. */ - const groups = new Map(); - - // Place each commit in the list into its group. - commits.forEach(commit => { - const key = commit.npmScope ? `${commit.npmScope}/${commit.scope}` : commit.scope; - const groupCommits = groups.get(key) || []; - groups.set(key, groupCommits); - groupCommits.push(commit); - }); - - /** - * Array of CommitGroups containing the discovered commit groups. Sorted in alphanumeric order - * of the group title. - */ - const commitGroups = Array.from(groups.entries()) - .map(([title, commits]) => ({title, commits})) - .sort((a, b) => a.title > b.title ? 1 : a.title < b.title ? -1 : 0); - - // If the configuration provides a sorting order, updated the sorted list of group keys to - // satisfy the order of the groups provided in the list with any groups not found in the list at - // the end of the sorted list. - if (this.groupOrder.length) { - for (const groupTitle of this.groupOrder.reverse()) { - const currentIdx = commitGroups.findIndex(k => k.title === groupTitle); - if (currentIdx !== -1) { - const removedGroups = commitGroups.splice(currentIdx, 1); - commitGroups.splice(0, 0, ...removedGroups); - } - } - } - return commitGroups; - } - - /** - * A filter function for filtering a list of commits to only include commits which should appear - * in release notes. - */ - includeInReleaseNotes() { - return (commit: CommitFromGitLog) => { - if (!typesToIncludeInReleaseNotes.includes(commit.type)) { - return false; - } - - if (this.hiddenScopes.includes(commit.scope)) { - return false; - } - return true; - }; - } - - /** - * A filter function for filtering a list of commits to only include commits which contain a - * truthy value, or for arrays an array with 1 or more elements, for the provided field. - */ - contains(field: keyof CommitFromGitLog) { - return (commit: CommitFromGitLog) => { - const fieldValue = commit[field]; - if (!fieldValue) { - return false; - } - - if (Array.isArray(fieldValue) && fieldValue.length === 0) { - return false; - } - return true; - }; - } - - /** - * A filter function for filtering a list of commits to only include commits which contain a - * unique value for the provided field across all commits in the list. - */ - unique(field: keyof CommitFromGitLog) { - const set = new Set(); - return (commit: CommitFromGitLog) => { - const include = !set.has(commit[field]); - set.add(commit[field]); - return include; - }; - } - - /** - * Convert a commit object to a Markdown link. - */ - commitToLink(commit: CommitFromGitLog): string { - const url = `https://github.com/${this.data.github.owner}/${this.data.github.name}/commit/${ - commit.hash}`; - return `[${commit.shortHash}](${url})`; - } - - /** - * Convert a pull request number to a Markdown link. - */ - pullRequestToLink(prNumber: number): string { - const url = - `https://github.com/${this.data.github.owner}/${this.data.github.name}/pull/${prNumber}`; - return `[#${prNumber}](${url})`; - } - - /** - * Transform a commit message header by replacing the parenthesized pull request reference at the - * end of the line (which is added by merge tooling) to a Markdown link. - */ - replaceCommitHeaderPullRequestNumber(header: string): string { - return header.replace(/\(#(\d+)\)$/, (_, g) => `(${this.pullRequestToLink(+g)})`); - } -} - - -/** - * Builds a date stamp for stamping in release notes. - * - * Uses the current date, or a provided date in the format of YYYY-MM-DD, i.e. 1970-11-05. - */ -export function buildDateStamp(date = new Date()) { - const year = `${date.getFullYear()}`; - const month = `${(date.getMonth() + 1)}`.padStart(2, '0'); - const day = `${date.getDate()}`.padStart(2, '0'); - - return [year, month, day].join('-'); -} diff --git a/dev-infra/release/notes/release-notes.ts b/dev-infra/release/notes/release-notes.ts deleted file mode 100644 index 74b46db024e8a4..00000000000000 --- a/dev-infra/release/notes/release-notes.ts +++ /dev/null @@ -1,92 +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 {render} from 'ejs'; -import * as semver from 'semver'; -import {CommitFromGitLog} from '../../commit-message/parse'; - -import {getCommitsInRange} from '../../commit-message/utils'; -import {promptInput} from '../../utils/console'; -import {GitClient} from '../../utils/git/git-client'; -import {DevInfraReleaseConfig, getReleaseConfig, ReleaseNotesConfig} from '../config/index'; -import {RenderContext} from './context'; - -import changelogTemplate from './templates/changelog'; -import githubReleaseTemplate from './templates/github-release'; - -/** Release note generation. */ -export class ReleaseNotes { - static async fromRange(version: semver.SemVer, startingRef: string, endingRef: string) { - return new ReleaseNotes(version, startingRef, endingRef); - } - - /** An instance of GitClient. */ - private git = GitClient.get(); - /** The RenderContext to be used during rendering. */ - private renderContext: RenderContext|undefined; - /** The title to use for the release. */ - private title: string|false|undefined; - /** A promise resolving to a list of Commits since the latest semver tag on the branch. */ - private commits: Promise = - this.getCommitsInRange(this.startingRef, this.endingRef); - /** The configuration for release notes. */ - private config: ReleaseNotesConfig = this.getReleaseConfig().releaseNotes; - - protected constructor( - public version: semver.SemVer, private startingRef: string, private endingRef: string) {} - - /** Retrieve the release note generated for a Github Release. */ - async getGithubReleaseEntry(): Promise { - return render(githubReleaseTemplate, await this.generateRenderContext(), {rmWhitespace: true}); - } - - /** Retrieve the release note generated for a CHANGELOG entry. */ - async getChangelogEntry() { - return render(changelogTemplate, await this.generateRenderContext(), {rmWhitespace: true}); - } - - /** - * Prompt the user for a title for the release, if the project's configuration is defined to use a - * title. - */ - async promptForReleaseTitle() { - if (this.title === undefined) { - if (this.config.useReleaseTitle) { - this.title = await promptInput('Please provide a title for the release:'); - } else { - this.title = false; - } - } - return this.title; - } - - /** Build the render context data object for constructing the RenderContext instance. */ - private async generateRenderContext(): Promise { - if (!this.renderContext) { - this.renderContext = new RenderContext({ - commits: await this.commits, - github: this.git.remoteConfig, - version: this.version.format(), - groupOrder: this.config.groupOrder, - hiddenScopes: this.config.hiddenScopes, - title: await this.promptForReleaseTitle(), - }); - } - return this.renderContext; - } - - - // These methods are used for access to the utility functions while allowing them to be - // overwritten in subclasses during testing. - protected async getCommitsInRange(from: string, to?: string) { - return getCommitsInRange(from, to); - } - - protected getReleaseConfig(config?: Partial) { - return getReleaseConfig(config); - } -} diff --git a/dev-infra/release/notes/templates/changelog.ts b/dev-infra/release/notes/templates/changelog.ts deleted file mode 100644 index 462c3ac331fc07..00000000000000 --- a/dev-infra/release/notes/templates/changelog.ts +++ /dev/null @@ -1,92 +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 default ` - -# <%- version %><% if (title) { %> "<%- title %>"<% } %> (<%- dateStamp %>) - -<%_ -const commitsInChangelog = commits.filter(includeInReleaseNotes()); -for (const group of asCommitGroups(commitsInChangelog)) { -_%> - -### <%- group.title %> -| Commit | Description | -| -- | -- | -<%_ - for (const commit of group.commits) { -_%> -| <%- commitToLink(commit) %> | <%- replaceCommitHeaderPullRequestNumber(commit.header) %> | -<%_ - } -} -_%> - -<%_ -const breakingChanges = commits.filter(contains('breakingChanges')); -if (breakingChanges.length) { -_%> -## Breaking Changes - -<%_ - for (const group of asCommitGroups(breakingChanges)) { -_%> -### <%- group.title %> - -<%_ - for (const commit of group.commits) { -_%> -<%- commit.breakingChanges[0].text %> - -<%_ - } - } -} -_%> - -<%_ -const deprecations = commits.filter(contains('deprecations')); -if (deprecations.length) { -_%> -## Deprecations -<%_ - for (const group of asCommitGroups(deprecations)) { -_%> -### <%- group.title %> - -<%_ - for (const commit of group.commits) { -_%> -<%- commit.deprecations[0].text %> -<%_ - } - } -} -_%> - -<%_ -const botsAuthorName = ['dependabot[bot]', 'Renovate Bot']; -const authors = commits - .filter(unique('author')) - .map(c => c.author) - .filter(a => !botsAuthorName.includes(a)) - .sort(); -if (authors.length === 1) { -_%> -## Special Thanks: -<%- authors[0]%> -<%_ -} -if (authors.length > 1) { -_%> -## Special Thanks: -<%- authors.slice(0, -1).join(', ') %> and <%- authors.slice(-1)[0] %> -<%_ -} -_%> -`; diff --git a/dev-infra/release/notes/templates/github-release.ts b/dev-infra/release/notes/templates/github-release.ts deleted file mode 100644 index 8b96d62531e357..00000000000000 --- a/dev-infra/release/notes/templates/github-release.ts +++ /dev/null @@ -1,87 +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 default ` - -# <%- version %><% if (title) { %> "<%- title %>"<% } %> (<%- dateStamp %>) - -<%_ -const commitsInChangelog = commits.filter(includeInReleaseNotes()); -for (const group of asCommitGroups(commitsInChangelog)) { -_%> - -### <%- group.title %> -| Commit | Description | -| -- | -- | -<%_ - for (const commit of group.commits) { -_%> -| <%- commit.shortHash %> | <%- commit.header %> | -<%_ - } -} -_%> - -<%_ -const breakingChanges = commits.filter(contains('breakingChanges')); -if (breakingChanges.length) { -_%> -## Breaking Changes - -<%_ - for (const group of asCommitGroups(breakingChanges)) { -_%> -### <%- group.title %> - -<%_ - for (const commit of group.commits) { -_%> -<%- commit.breakingChanges[0].text %> - -<%_ - } - } -} -_%> - -<%_ -const deprecations = commits.filter(contains('deprecations')); -if (deprecations.length) { -_%> -## Deprecations -<%_ - for (const group of asCommitGroups(deprecations)) { -_%> -### <%- group.title %> - -<%_ - for (const commit of group.commits) { -_%> -<%- commit.deprecations[0].text %> -<%_ - } - } -} -_%> - -<%_ -const authors = commits.filter(unique('author')).map(c => c.author).sort(); -if (authors.length === 1) { -_%> -## Special Thanks: -<%- authors[0]%> -<%_ -} -if (authors.length > 1) { -_%> -## Special Thanks: -<%- authors.slice(0, -1).join(', ') %> and <%- authors.slice(-1)[0] %> -<%_ -} -_%> -`; diff --git a/dev-infra/release/publish/BUILD.bazel b/dev-infra/release/publish/BUILD.bazel deleted file mode 100644 index ec03dfbdf7b2c6..00000000000000 --- a/dev-infra/release/publish/BUILD.bazel +++ /dev/null @@ -1,28 +0,0 @@ -load("//dev-infra:defaults.bzl", "ts_library") - -ts_library( - name = "publish", - srcs = glob([ - "**/*.ts", - ]), - visibility = ["//dev-infra:__subpackages__"], - deps = [ - "//dev-infra/commit-message", - "//dev-infra/pr/merge", - "//dev-infra/release/config", - "//dev-infra/release/notes", - "//dev-infra/release/versioning", - "//dev-infra/utils", - "@npm//@octokit/rest", - "@npm//@types/ejs", - "@npm//@types/inquirer", - "@npm//@types/node", - "@npm//@types/semver", - "@npm//@types/yargs", - "@npm//ejs", - "@npm//inquirer", - "@npm//ora", - "@npm//semver", - "@npm//typed-graphqlify", - ], -) diff --git a/dev-infra/release/publish/actions-error.ts b/dev-infra/release/publish/actions-error.ts deleted file mode 100644 index 517b7bcf47d6af..00000000000000 --- a/dev-infra/release/publish/actions-error.ts +++ /dev/null @@ -1,29 +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 - */ - -/** Error that will be thrown if the user manually aborted a release action. */ -export class UserAbortedReleaseActionError extends Error { - constructor() { - super(); - // 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, UserAbortedReleaseActionError.prototype); - } -} - -/** Error that will be thrown if the action has been aborted due to a fatal error. */ -export class FatalReleaseActionError extends Error { - constructor() { - super(); - // 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, FatalReleaseActionError.prototype); - } -} diff --git a/dev-infra/release/publish/actions.ts b/dev-infra/release/publish/actions.ts deleted file mode 100644 index 68460ac0dbfb10..00000000000000 --- a/dev-infra/release/publish/actions.ts +++ /dev/null @@ -1,543 +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 {promises as fs} from 'fs'; -import * as ora from 'ora'; -import {join} from 'path'; -import * as semver from 'semver'; - -import {debug, error, green, info, promptConfirm, red, warn, yellow} from '../../utils/console'; -import {AuthenticatedGitClient} from '../../utils/git/authenticated-git-client'; -import {getListCommitsInBranchUrl, getRepositoryGitUrl} from '../../utils/git/github-urls'; -import {createExperimentalSemver} from '../../utils/semver'; -import {BuiltPackage, ReleaseConfig} from '../config/index'; -import {ReleaseNotes} from '../notes/release-notes'; -import {NpmDistTag} from '../versioning'; -import {ActiveReleaseTrains} from '../versioning/active-release-trains'; -import {runNpmPublish} from '../versioning/npm-publish'; - -import {FatalReleaseActionError, UserAbortedReleaseActionError} from './actions-error'; -import {getCommitMessageForRelease, getReleaseNoteCherryPickCommitMessage} from './commit-message'; -import {changelogPath, packageJsonPath, waitForPullRequestInterval} from './constants'; -import {invokeReleaseBuildCommand, invokeYarnInstallCommand} from './external-commands'; -import {findOwnedForksOfRepoQuery} from './graphql-queries'; -import {getPullRequestState} from './pull-request-state'; - -/** Interface describing a Github repository. */ -export interface GithubRepo { - owner: string; - name: string; -} - -/** Interface describing a Github pull request. */ -export interface PullRequest { - /** Unique id for the pull request (i.e. the PR number). */ - id: number; - /** URL that resolves to the pull request in Github. */ - url: string; - /** Fork containing the head branch of this pull request. */ - fork: GithubRepo; - /** Branch name in the fork that defines this pull request. */ - forkBranch: string; -} - -/** Constructor type for instantiating a release action */ -export interface ReleaseActionConstructor { - /** Whether the release action is currently active. */ - isActive(active: ActiveReleaseTrains, config: ReleaseConfig): Promise; - /** Constructs a release action. */ - new(...args: [ActiveReleaseTrains, AuthenticatedGitClient, ReleaseConfig, string]): T; -} - -/** - * Abstract base class for a release action. A release action is selectable by the caretaker - * if active, and can perform changes for releasing, such as staging a release, bumping the - * version, cherry-picking the changelog, branching off from master. etc. - */ -export abstract class ReleaseAction { - /** Whether the release action is currently active. */ - static isActive(_trains: ActiveReleaseTrains, _config: ReleaseConfig): Promise { - throw Error('Not implemented.'); - } - - /** Gets the description for a release action. */ - abstract getDescription(): Promise; - /** - * Performs the given release action. - * @throws {UserAbortedReleaseActionError} When the user manually aborted the action. - * @throws {FatalReleaseActionError} When the action has been aborted due to a fatal error. - */ - abstract perform(): Promise; - - /** Cached found fork of the configured project. */ - private _cachedForkRepo: GithubRepo|null = null; - - constructor( - protected active: ActiveReleaseTrains, protected git: AuthenticatedGitClient, - protected config: ReleaseConfig, protected projectDir: string) {} - - /** Retrieves the version in the project top-level `package.json` file. */ - private async getProjectVersion() { - const pkgJsonPath = join(this.projectDir, packageJsonPath); - const pkgJson = - JSON.parse(await fs.readFile(pkgJsonPath, 'utf8')) as {version: string, [key: string]: any}; - return new semver.SemVer(pkgJson.version); - } - - /** Updates the version in the project top-level `package.json` file. */ - protected async updateProjectVersion(newVersion: semver.SemVer) { - const pkgJsonPath = join(this.projectDir, packageJsonPath); - const pkgJson = - JSON.parse(await fs.readFile(pkgJsonPath, 'utf8')) as {version: string, [key: string]: any}; - pkgJson.version = newVersion.format(); - // Write the `package.json` file. Note that we add a trailing new line - // to avoid unnecessary diff. IDEs usually add a trailing new line. - await fs.writeFile(pkgJsonPath, `${JSON.stringify(pkgJson, null, 2)}\n`); - info(green(` ✓ Updated project version to ${pkgJson.version}`)); - } - - /** Gets the most recent commit of a specified branch. */ - private async _getCommitOfBranch(branchName: string): Promise { - const {data: {commit}} = - await this.git.github.repos.getBranch({...this.git.remoteParams, branch: branchName}); - return commit.sha; - } - - /** Verifies that the latest commit for the given branch is passing all statuses. */ - protected async verifyPassingGithubStatus(branchName: string) { - const commitSha = await this._getCommitOfBranch(branchName); - const {data: {state}} = await this.git.github.repos.getCombinedStatusForRef( - {...this.git.remoteParams, ref: commitSha}); - const branchCommitsUrl = getListCommitsInBranchUrl(this.git, branchName); - - if (state === 'failure') { - error( - red(` ✘ Cannot stage release. Commit "${commitSha}" does not pass all github ` + - 'status checks. Please make sure this commit passes all checks before re-running.')); - error(` Please have a look at: ${branchCommitsUrl}`); - - if (await promptConfirm('Do you want to ignore the Github status and proceed?')) { - info(yellow( - ' ⚠ Upstream commit is failing CI checks, but status has been forcibly ignored.')); - return; - } - throw new UserAbortedReleaseActionError(); - } else if (state === 'pending') { - error( - red(` ✘ Commit "${commitSha}" still has pending github statuses that ` + - 'need to succeed before staging a release.')); - error(red(` Please have a look at: ${branchCommitsUrl}`)); - if (await promptConfirm('Do you want to ignore the Github status and proceed?')) { - info(yellow(' ⚠ Upstream commit is pending CI, but status has been forcibly ignored.')); - return; - } - throw new UserAbortedReleaseActionError(); - } - - info(green(' ✓ Upstream commit is passing all github status checks.')); - } - - - /** - * Prompts the user for potential release notes edits that need to be made. Once - * confirmed, a new commit for the release point is created. - */ - protected async waitForEditsAndCreateReleaseCommit(newVersion: semver.SemVer) { - info(yellow( - ' ⚠ Please review the changelog and ensure that the log contains only changes ' + - 'that apply to the public API surface. Manual changes can be made. When done, please ' + - 'proceed with the prompt below.')); - - if (!await promptConfirm('Do you want to proceed and commit the changes?')) { - throw new UserAbortedReleaseActionError(); - } - - // Commit message for the release point. - const commitMessage = getCommitMessageForRelease(newVersion); - // Create a release staging commit including changelog and version bump. - await this.createCommit(commitMessage, [packageJsonPath, changelogPath]); - - info(green(` ✓ Created release commit for: "${newVersion}".`)); - } - - /** - * Gets an owned fork for the configured project of the authenticated user. Aborts the - * process with an error if no fork could be found. Also caches the determined fork - * repository as the authenticated user cannot change during action execution. - */ - private async _getForkOfAuthenticatedUser(): Promise { - if (this._cachedForkRepo !== null) { - return this._cachedForkRepo; - } - - const {owner, name} = this.git.remoteConfig; - const result = await this.git.github.graphql(findOwnedForksOfRepoQuery, {owner, name}); - const forks = result.repository.forks.nodes; - - if (forks.length === 0) { - error(red(' ✘ Unable to find fork for currently authenticated user.')); - error(red(` Please ensure you created a fork of: ${owner}/${name}.`)); - throw new FatalReleaseActionError(); - } - - const fork = forks[0]; - return this._cachedForkRepo = {owner: fork.owner.login, name: fork.name}; - } - - /** Checks whether a given branch name is reserved in the specified repository. */ - private async _isBranchNameReservedInRepo(repo: GithubRepo, name: string): Promise { - try { - await this.git.github.repos.getBranch({owner: repo.owner, repo: repo.name, branch: name}); - return true; - } catch (e) { - // If the error has a `status` property set to `404`, then we know that the branch - // does not exist. Otherwise, it might be an API error that we want to report/re-throw. - if (e.status === 404) { - return false; - } - throw e; - } - } - - /** Finds a non-reserved branch name in the repository with respect to a base name. */ - private async _findAvailableBranchName(repo: GithubRepo, baseName: string): Promise { - let currentName = baseName; - let suffixNum = 0; - while (await this._isBranchNameReservedInRepo(repo, currentName)) { - suffixNum++; - currentName = `${baseName}_${suffixNum}`; - } - return currentName; - } - - /** - * Creates a local branch from the current Git `HEAD`. Will override - * existing branches in case of a collision. - */ - protected async createLocalBranchFromHead(branchName: string) { - this.git.run(['checkout', '-q', '-B', branchName]); - } - - /** Pushes the current Git `HEAD` to the given remote branch in the configured project. */ - protected async pushHeadToRemoteBranch(branchName: string) { - // Push the local `HEAD` to the remote branch in the configured project. - this.git.run(['push', '-q', this.git.getRepoGitUrl(), `HEAD:refs/heads/${branchName}`]); - } - - /** - * Pushes the current Git `HEAD` to a fork for the configured project that is owned by - * the authenticated user. If the specified branch name exists in the fork already, a - * unique one will be generated based on the proposed name to avoid collisions. - * @param proposedBranchName Proposed branch name for the fork. - * @param trackLocalBranch Whether the fork branch should be tracked locally. i.e. whether - * a local branch with remote tracking should be set up. - * @returns The fork and branch name containing the pushed changes. - */ - private async _pushHeadToFork(proposedBranchName: string, trackLocalBranch: boolean): - Promise<{fork: GithubRepo, branchName: string}> { - const fork = await this._getForkOfAuthenticatedUser(); - // Compute a repository URL for pushing to the fork. Note that we want to respect - // the SSH option from the dev-infra github configuration. - const repoGitUrl = - getRepositoryGitUrl({...fork, useSsh: this.git.remoteConfig.useSsh}, this.git.githubToken); - const branchName = await this._findAvailableBranchName(fork, proposedBranchName); - const pushArgs: string[] = []; - // If a local branch should track the remote fork branch, create a branch matching - // the remote branch. Later with the `git push`, the remote is set for the branch. - if (trackLocalBranch) { - await this.createLocalBranchFromHead(branchName); - pushArgs.push('--set-upstream'); - } - // Push the local `HEAD` to the remote branch in the fork. - this.git.run(['push', '-q', repoGitUrl, `HEAD:refs/heads/${branchName}`, ...pushArgs]); - return {fork, branchName}; - } - - /** - * Pushes changes to a fork for the configured project that is owned by the currently - * authenticated user. A pull request is then created for the pushed changes on the - * configured project that targets the specified target branch. - * @returns An object describing the created pull request. - */ - protected async pushChangesToForkAndCreatePullRequest( - targetBranch: string, proposedForkBranchName: string, title: string, - body?: string): Promise { - const repoSlug = `${this.git.remoteParams.owner}/${this.git.remoteParams.repo}`; - const {fork, branchName} = await this._pushHeadToFork(proposedForkBranchName, true); - const {data} = await this.git.github.pulls.create({ - ...this.git.remoteParams, - head: `${fork.owner}:${branchName}`, - base: targetBranch, - body, - title, - }); - - // Add labels to the newly created PR if provided in the configuration. - if (this.config.releasePrLabels !== undefined) { - await this.git.github.issues.addLabels({ - ...this.git.remoteParams, - issue_number: data.number, - labels: this.config.releasePrLabels, - }); - } - - info(green(` ✓ Created pull request #${data.number} in ${repoSlug}.`)); - return { - id: data.number, - url: data.html_url, - fork, - forkBranch: branchName, - }; - } - - /** - * Waits for the given pull request to be merged. Default interval for checking the Github - * API is 10 seconds (to not exceed any rate limits). If the pull request is closed without - * merge, the script will abort gracefully (considering a manual user abort). - */ - protected async waitForPullRequestToBeMerged( - {id}: PullRequest, interval = waitForPullRequestInterval): Promise { - return new Promise((resolve, reject) => { - debug(`Waiting for pull request #${id} to be merged.`); - - const spinner = ora.call(undefined).start(`Waiting for pull request #${id} to be merged.`); - const intervalId = setInterval(async () => { - const prState = await getPullRequestState(this.git, id); - if (prState === 'merged') { - spinner.stop(); - info(green(` ✓ Pull request #${id} has been merged.`)); - clearInterval(intervalId); - resolve(); - } else if (prState === 'closed') { - spinner.stop(); - warn(yellow(` ✘ Pull request #${id} has been closed.`)); - clearInterval(intervalId); - reject(new UserAbortedReleaseActionError()); - } - }, interval); - }); - } - - /** - * Prepend releases notes for a version published in a given branch to the changelog in - * the current Git `HEAD`. This is useful for cherry-picking the changelog. - * @returns A boolean indicating whether the release notes have been prepended. - */ - protected async prependReleaseNotesToChangelog(releaseNotes: ReleaseNotes): Promise { - const localChangelogPath = join(this.projectDir, changelogPath); - const localChangelog = await fs.readFile(localChangelogPath, 'utf8'); - const releaseNotesEntry = await releaseNotes.getChangelogEntry(); - await fs.writeFile(localChangelogPath, `${releaseNotesEntry}\n\n${localChangelog}`); - info(green(` ✓ Updated the changelog to capture changes for "${releaseNotes.version}".`)); - } - - /** Checks out an upstream branch with a detached head. */ - protected async checkoutUpstreamBranch(branchName: string) { - this.git.run(['fetch', '-q', this.git.getRepoGitUrl(), branchName]); - this.git.run(['checkout', '-q', 'FETCH_HEAD', '--detach']); - } - - /** - * Creates a commit for the specified files with the given message. - * @param message Message for the created commit - * @param files List of project-relative file paths to be commited. - */ - protected async createCommit(message: string, files: string[]) { - this.git.run(['commit', '-q', '--no-verify', '-m', message, ...files]); - } - - - /** - * Stages the specified new version for the current branch and creates a - * pull request that targets the given base branch. - * @returns an object describing the created pull request. - */ - protected async stageVersionForBranchAndCreatePullRequest( - newVersion: semver.SemVer, pullRequestBaseBranch: string): - Promise<{releaseNotes: ReleaseNotes, pullRequest: PullRequest}> { - /** - * The current version of the project for the branch from the root package.json. This must be - * retrieved prior to updating the project version. - */ - const currentVersion = this.git.getMatchingTagForSemver(await this.getProjectVersion()); - const releaseNotes = await ReleaseNotes.fromRange(newVersion, currentVersion, 'HEAD'); - await this.updateProjectVersion(newVersion); - await this.prependReleaseNotesToChangelog(releaseNotes); - await this.waitForEditsAndCreateReleaseCommit(newVersion); - - const pullRequest = await this.pushChangesToForkAndCreatePullRequest( - pullRequestBaseBranch, `release-stage-${newVersion}`, - `Bump version to "v${newVersion}" with changelog.`); - - info(green(' ✓ Release staging pull request has been created.')); - info(yellow(` Please ask team members to review: ${pullRequest.url}.`)); - - return {releaseNotes, pullRequest}; - } - - /** - * Checks out the specified target branch, verifies its CI status and stages - * the specified new version in order to create a pull request. - * @returns an object describing the created pull request. - */ - protected async checkoutBranchAndStageVersion(newVersion: semver.SemVer, stagingBranch: string): - Promise<{releaseNotes: ReleaseNotes, pullRequest: PullRequest}> { - await this.verifyPassingGithubStatus(stagingBranch); - await this.checkoutUpstreamBranch(stagingBranch); - return await this.stageVersionForBranchAndCreatePullRequest(newVersion, stagingBranch); - } - - /** - * Cherry-picks the release notes of a version that have been pushed to a given branch - * into the `next` primary development branch. A pull request is created for this. - * @returns a boolean indicating successful creation of the cherry-pick pull request. - */ - protected async cherryPickChangelogIntoNextBranch( - releaseNotes: ReleaseNotes, stagingBranch: string): Promise { - const nextBranch = this.active.next.branchName; - const commitMessage = getReleaseNoteCherryPickCommitMessage(releaseNotes.version); - - // Checkout the next branch. - await this.checkoutUpstreamBranch(nextBranch); - - await this.prependReleaseNotesToChangelog(releaseNotes); - - // Create a changelog cherry-pick commit. - await this.createCommit(commitMessage, [changelogPath]); - info(green(` ✓ Created changelog cherry-pick commit for: "${releaseNotes.version}".`)); - - // Create a cherry-pick pull request that should be merged by the caretaker. - const pullRequest = await this.pushChangesToForkAndCreatePullRequest( - nextBranch, `changelog-cherry-pick-${releaseNotes.version}`, commitMessage, - `Cherry-picks the changelog from the "${stagingBranch}" branch to the next ` + - `branch (${nextBranch}).`); - - info(green( - ` ✓ Pull request for cherry-picking the changelog into "${nextBranch}" ` + - 'has been created.')); - info(yellow(` Please ask team members to review: ${pullRequest.url}.`)); - - // Wait for the Pull Request to be merged. - await this.waitForPullRequestToBeMerged(pullRequest); - - return true; - } - - /** - * Creates a Github release for the specified version in the configured project. - * The release is created by tagging the specified commit SHA. - */ - private async _createGithubReleaseForVersion( - releaseNotes: ReleaseNotes, versionBumpCommitSha: string, prerelease: boolean) { - const tagName = releaseNotes.version.format(); - await this.git.github.git.createRef({ - ...this.git.remoteParams, - ref: `refs/tags/${tagName}`, - sha: versionBumpCommitSha, - }); - info(green(` ✓ Tagged v${releaseNotes.version} release upstream.`)); - - await this.git.github.repos.createRelease({ - ...this.git.remoteParams, - name: `v${releaseNotes.version}`, - tag_name: tagName, - prerelease, - body: await releaseNotes.getGithubReleaseEntry(), - }); - info(green(` ✓ Created v${releaseNotes.version} release in Github.`)); - } - - /** - * Builds and publishes the given version in the specified branch. - * @param releaseNotes The release notes for the version being published. - * @param publishBranch Name of the branch that contains the new version. - * @param npmDistTag NPM dist tag where the version should be published to. - */ - protected async buildAndPublish( - releaseNotes: ReleaseNotes, publishBranch: string, npmDistTag: NpmDistTag) { - const versionBumpCommitSha = await this._getCommitOfBranch(publishBranch); - - if (!await this._isCommitForVersionStaging(releaseNotes.version, versionBumpCommitSha)) { - error(red(` ✘ Latest commit in "${publishBranch}" branch is not a staging commit.`)); - error(red(' Please make sure the staging pull request has been merged.')); - throw new FatalReleaseActionError(); - } - - // Checkout the publish branch and build the release packages. - await this.checkoutUpstreamBranch(publishBranch); - - // Install the project dependencies for the publish branch, and then build the release - // packages. Note that we do not directly call the build packages function from the release - // config. We only want to build and publish packages that have been configured in the given - // publish branch. e.g. consider we publish patch version and a new package has been - // created in the `next` branch. The new package would not be part of the patch branch, - // so we cannot build and publish it. - await invokeYarnInstallCommand(this.projectDir); - const builtPackages = await invokeReleaseBuildCommand(); - - // Verify the packages built are the correct version. - await this._verifyPackageVersions(releaseNotes.version, builtPackages); - - // Create a Github release for the new version. - await this._createGithubReleaseForVersion( - releaseNotes, versionBumpCommitSha, npmDistTag === 'next'); - - // Walk through all built packages and publish them to NPM. - for (const builtPackage of builtPackages) { - await this._publishBuiltPackageToNpm(builtPackage, npmDistTag); - } - - info(green(' ✓ Published all packages successfully')); - } - - /** Publishes the given built package to NPM with the specified NPM dist tag. */ - private async _publishBuiltPackageToNpm(pkg: BuiltPackage, npmDistTag: NpmDistTag) { - debug(`Starting publish of "${pkg.name}".`); - const spinner = ora.call(undefined).start(`Publishing "${pkg.name}"`); - - try { - await runNpmPublish(pkg.outputPath, npmDistTag, this.config.publishRegistry); - spinner.stop(); - info(green(` ✓ Successfully published "${pkg.name}.`)); - } catch (e) { - spinner.stop(); - error(e); - error(red(` ✘ An error occurred while publishing "${pkg.name}".`)); - throw new FatalReleaseActionError(); - } - } - - /** Checks whether the given commit represents a staging commit for the specified version. */ - private async _isCommitForVersionStaging(version: semver.SemVer, commitSha: string) { - const {data} = - await this.git.github.repos.getCommit({...this.git.remoteParams, ref: commitSha}); - return data.commit.message.startsWith(getCommitMessageForRelease(version)); - } - - /** Verify the version of each generated package exact matches the specified version. */ - private async _verifyPackageVersions(version: semver.SemVer, packages: BuiltPackage[]) { - /** Experimental equivalent version for packages created with the provided version. */ - const experimentalVersion = createExperimentalSemver(version); - - for (const pkg of packages) { - const {version: packageJsonVersion} = - JSON.parse(await fs.readFile(join(pkg.outputPath, 'package.json'), 'utf8')) as - {version: string, [key: string]: any}; - - const mismatchesVersion = version.compare(packageJsonVersion) !== 0; - const mismatchesExperimental = experimentalVersion.compare(packageJsonVersion) !== 0; - - if (mismatchesExperimental && mismatchesVersion) { - error(red('The built package version does not match the version being released.')); - error(` Release Version: ${version.version} (${experimentalVersion.version})`); - error(` Generated Version: ${packageJsonVersion}`); - throw new FatalReleaseActionError(); - } - } - } -} diff --git a/dev-infra/release/publish/actions/branch-off-next-branch.ts b/dev-infra/release/publish/actions/branch-off-next-branch.ts deleted file mode 100644 index 5b64b3a6e1dd43..00000000000000 --- a/dev-infra/release/publish/actions/branch-off-next-branch.ts +++ /dev/null @@ -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 * as semver from 'semver'; - -import {green, info, yellow} from '../../../utils/console'; -import {semverInc} from '../../../utils/semver'; -import {ReleaseNotes} from '../../notes/release-notes'; -import {ActiveReleaseTrains} from '../../versioning/active-release-trains'; -import {computeNewPrereleaseVersionForNext} from '../../versioning/next-prerelease-version'; -import {ReleaseAction} from '../actions'; -import {getCommitMessageForExceptionalNextVersionBump, getReleaseNoteCherryPickCommitMessage} from '../commit-message'; -import {changelogPath, packageJsonPath} from '../constants'; - -/** - * Base action that can be used to move the next release-train into the feature-freeze or - * release-candidate phase. This means that a new version branch is created from the next - * branch, and a new pre-release (either RC or another `next`) is cut indicating the new phase. - */ -export abstract class BranchOffNextBranchBaseAction extends ReleaseAction { - /** - * Phase which the release-train currently in the `next` phase will move into. - * - * Note that we only allow for a next version to branch into feature-freeze or - * directly into the release-candidate phase. A stable version cannot be released - * without release-candidate. - */ - abstract newPhaseName: 'feature-freeze'|'release-candidate'; - - override async getDescription() { - const {branchName} = this.active.next; - const newVersion = await this._computeNewVersion(); - return `Move the "${branchName}" branch into ${this.newPhaseName} phase (v${newVersion}).`; - } - - override async perform() { - const newVersion = await this._computeNewVersion(); - const newBranch = `${newVersion.major}.${newVersion.minor}.x`; - - // Branch-off the next branch into a new version branch. - await this._createNewVersionBranchFromNext(newBranch); - - // Stage the new version for the newly created branch, and push changes to a - // fork in order to create a staging pull request. Note that we re-use the newly - // created branch instead of re-fetching from the upstream. - const {pullRequest, releaseNotes} = - await this.stageVersionForBranchAndCreatePullRequest(newVersion, newBranch); - - // Wait for the staging PR to be merged. Then build and publish the feature-freeze next - // pre-release. Finally, cherry-pick the release notes into the next branch in combination - // with bumping the version to the next minor too. - await this.waitForPullRequestToBeMerged(pullRequest); - await this.buildAndPublish(releaseNotes, newBranch, 'next'); - await this._createNextBranchUpdatePullRequest(releaseNotes, newVersion); - } - - /** Computes the new version for the release-train being branched-off. */ - private async _computeNewVersion() { - if (this.newPhaseName === 'feature-freeze') { - return computeNewPrereleaseVersionForNext(this.active, this.config); - } else { - return semverInc(this.active.next.version, 'prerelease', 'rc'); - } - } - - /** Creates a new version branch from the next branch. */ - private async _createNewVersionBranchFromNext(newBranch: string) { - const {branchName: nextBranch} = this.active.next; - await this.verifyPassingGithubStatus(nextBranch); - await this.checkoutUpstreamBranch(nextBranch); - await this.createLocalBranchFromHead(newBranch); - await this.pushHeadToRemoteBranch(newBranch); - info(green(` ✓ Version branch "${newBranch}" created.`)); - } - - /** - * Creates a pull request for the next branch that bumps the version to the next - * minor, and cherry-picks the changelog for the newly branched-off release-candidate - * or feature-freeze version. - */ - private async _createNextBranchUpdatePullRequest( - releaseNotes: ReleaseNotes, newVersion: semver.SemVer) { - const {branchName: nextBranch, version} = this.active.next; - // We increase the version for the next branch to the next minor. The team can decide - // later if they want next to be a major through the `Configure Next as Major` release action. - const newNextVersion = semver.parse(`${version.major}.${version.minor + 1}.0-next.0`)!; - const bumpCommitMessage = getCommitMessageForExceptionalNextVersionBump(newNextVersion); - - await this.checkoutUpstreamBranch(nextBranch); - await this.updateProjectVersion(newNextVersion); - - // Create an individual commit for the next version bump. The changelog should go into - // a separate commit that makes it clear where the changelog is cherry-picked from. - await this.createCommit(bumpCommitMessage, [packageJsonPath]); - - await this.prependReleaseNotesToChangelog(releaseNotes); - - const commitMessage = getReleaseNoteCherryPickCommitMessage(releaseNotes.version); - - await this.createCommit(commitMessage, [changelogPath]); - - let nextPullRequestMessage = `The previous "next" release-train has moved into the ` + - `${this.newPhaseName} phase. This PR updates the next branch to the subsequent ` + - `release-train.\n\nAlso this PR cherry-picks the changelog for ` + - `v${newVersion} into the ${nextBranch} branch so that the changelog is up to date.`; - - const nextUpdatePullRequest = await this.pushChangesToForkAndCreatePullRequest( - nextBranch, `next-release-train-${newNextVersion}`, - `Update next branch to reflect new release-train "v${newNextVersion}".`, - nextPullRequestMessage); - - info(green(` ✓ Pull request for updating the "${nextBranch}" branch has been created.`)); - info(yellow(` Please ask team members to review: ${nextUpdatePullRequest.url}.`)); - } -} diff --git a/dev-infra/release/publish/actions/configure-next-as-major.ts b/dev-infra/release/publish/actions/configure-next-as-major.ts deleted file mode 100644 index f3003f29758e0c..00000000000000 --- a/dev-infra/release/publish/actions/configure-next-as-major.ts +++ /dev/null @@ -1,53 +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 semver from 'semver'; - -import {green, info, yellow} from '../../../utils/console'; -import {ActiveReleaseTrains} from '../../versioning/active-release-trains'; -import {ReleaseAction} from '../actions'; -import {getCommitMessageForNextBranchMajorSwitch} from '../commit-message'; -import {packageJsonPath} from '../constants'; - -/** - * Release action that configures the active next release-train to be for a major - * version. This means that major changes can land in the next branch. - */ -export class ConfigureNextAsMajorAction extends ReleaseAction { - private _newVersion = semver.parse(`${this.active.next.version.major + 1}.0.0-next.0`)!; - - override async getDescription() { - const {branchName} = this.active.next; - const newVersion = this._newVersion; - return `Configure the "${branchName}" branch to be released as major (v${newVersion}).`; - } - - override async perform() { - const {branchName} = this.active.next; - const newVersion = this._newVersion; - - await this.verifyPassingGithubStatus(branchName); - await this.checkoutUpstreamBranch(branchName); - await this.updateProjectVersion(newVersion); - await this.createCommit( - getCommitMessageForNextBranchMajorSwitch(newVersion), [packageJsonPath]); - const pullRequest = await this.pushChangesToForkAndCreatePullRequest( - branchName, `switch-next-to-major-${newVersion}`, - `Configure next branch to receive major changes for v${newVersion}`); - - info(green(' ✓ Next branch update pull request has been created.')); - info(yellow(` Please ask team members to review: ${pullRequest.url}.`)); - } - - static override async isActive(active: ActiveReleaseTrains) { - // The `next` branch can always be switched to a major version, unless it already - // is targeting a new major. A major can contain minor changes, so we can always - // change the target from a minor to a major. - return !active.next.isMajor; - } -} diff --git a/dev-infra/release/publish/actions/cut-lts-patch.ts b/dev-infra/release/publish/actions/cut-lts-patch.ts deleted file mode 100644 index 940bd257ef1593..00000000000000 --- a/dev-infra/release/publish/actions/cut-lts-patch.ts +++ /dev/null @@ -1,83 +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 {ListChoiceOptions, prompt} from 'inquirer'; - -import {semverInc} from '../../../utils/semver'; -import {ActiveReleaseTrains} from '../../versioning/active-release-trains'; -import {fetchLongTermSupportBranchesFromNpm, LtsBranch} from '../../versioning/long-term-support'; -import {ReleaseAction} from '../actions'; - -/** - * Release action that cuts a new patch release for an active release-train in the long-term - * support phase. The patch segment is incremented. The changelog is generated for the new - * patch version, but also needs to be cherry-picked into the next development branch. - */ -export class CutLongTermSupportPatchAction extends ReleaseAction { - /** Promise resolving an object describing long-term support branches. */ - ltsBranches = fetchLongTermSupportBranchesFromNpm(this.config); - - override async getDescription() { - const {active} = await this.ltsBranches; - return `Cut a new release for an active LTS branch (${active.length} active).`; - } - - override async perform() { - const ltsBranch = await this._promptForTargetLtsBranch(); - const newVersion = semverInc(ltsBranch.version, 'patch'); - const {pullRequest, releaseNotes} = - await this.checkoutBranchAndStageVersion(newVersion, ltsBranch.name); - - await this.waitForPullRequestToBeMerged(pullRequest); - await this.buildAndPublish(releaseNotes, ltsBranch.name, ltsBranch.npmDistTag); - await this.cherryPickChangelogIntoNextBranch(releaseNotes, ltsBranch.name); - } - - /** Prompts the user to select an LTS branch for which a patch should but cut. */ - private async _promptForTargetLtsBranch(): Promise { - const {active, inactive} = await this.ltsBranches; - const activeBranchChoices = active.map(branch => this._getChoiceForLtsBranch(branch)); - - // If there are inactive LTS branches, we allow them to be selected. In some situations, - // patch releases are still cut for inactive LTS branches. e.g. when the LTS duration - // has been increased due to exceptional events () - if (inactive.length !== 0) { - activeBranchChoices.push({name: 'Inactive LTS versions (not recommended)', value: null}); - } - - const {activeLtsBranch, inactiveLtsBranch} = - await prompt<{activeLtsBranch: LtsBranch | null, inactiveLtsBranch: LtsBranch}>([ - { - name: 'activeLtsBranch', - type: 'list', - message: 'Please select a version for which you want to cut an LTS patch', - choices: activeBranchChoices, - }, - { - name: 'inactiveLtsBranch', - type: 'list', - when: o => o.activeLtsBranch === null, - message: 'Please select an inactive LTS version for which you want to cut an LTS patch', - choices: inactive.map(branch => this._getChoiceForLtsBranch(branch)), - } - ]); - return activeLtsBranch ?? inactiveLtsBranch; - } - - /** Gets an inquirer choice for the given LTS branch. */ - private _getChoiceForLtsBranch(branch: LtsBranch): ListChoiceOptions { - return {name: `v${branch.version.major} (from ${branch.name})`, value: branch}; - } - - static override async isActive(active: ActiveReleaseTrains) { - // LTS patch versions can be only cut if there are release trains in LTS phase. - // This action is always selectable as we support publishing of old LTS branches, - // and have prompt for selecting an LTS branch when the action performs. - return true; - } -} diff --git a/dev-infra/release/publish/actions/cut-new-patch.ts b/dev-infra/release/publish/actions/cut-new-patch.ts deleted file mode 100644 index bb8702941f56db..00000000000000 --- a/dev-infra/release/publish/actions/cut-new-patch.ts +++ /dev/null @@ -1,44 +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 {semverInc} from '../../../utils/semver'; -import {ActiveReleaseTrains} from '../../versioning/active-release-trains'; -import {ReleaseAction} from '../actions'; - -/** - * Release action that cuts a new patch release for the current latest release-train version - * branch (i.e. the patch branch). The patch segment is incremented. The changelog is generated - * for the new patch version, but also needs to be cherry-picked into the next development branch. - */ -export class CutNewPatchAction extends ReleaseAction { - private _newVersion = semverInc(this.active.latest.version, 'patch'); - - override async getDescription() { - const {branchName} = this.active.latest; - const newVersion = this._newVersion; - return `Cut a new patch release for the "${branchName}" branch (v${newVersion}).`; - } - - override async perform() { - const {branchName} = this.active.latest; - const newVersion = this._newVersion; - - const {pullRequest, releaseNotes} = - await this.checkoutBranchAndStageVersion(newVersion, branchName); - - await this.waitForPullRequestToBeMerged(pullRequest); - await this.buildAndPublish(releaseNotes, branchName, 'latest'); - await this.cherryPickChangelogIntoNextBranch(releaseNotes, branchName); - } - - static override async isActive(active: ActiveReleaseTrains) { - // Patch versions can be cut at any time. See: - // https://hackmd.io/2Le8leq0S6G_R5VEVTNK9A#Release-prompt-options. - return true; - } -} diff --git a/dev-infra/release/publish/actions/cut-next-prerelease.ts b/dev-infra/release/publish/actions/cut-next-prerelease.ts deleted file mode 100644 index 1be4f701778027..00000000000000 --- a/dev-infra/release/publish/actions/cut-next-prerelease.ts +++ /dev/null @@ -1,73 +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 semver from 'semver'; - -import {semverInc} from '../../../utils/semver'; -import {computeNewPrereleaseVersionForNext} from '../../versioning/next-prerelease-version'; -import {ReleaseTrain} from '../../versioning/release-trains'; -import {ReleaseAction} from '../actions'; - -/** - * Release action that cuts a prerelease for the next branch. A version in the next - * branch can have an arbitrary amount of next pre-releases. - */ -export class CutNextPrereleaseAction extends ReleaseAction { - /** Promise resolving with the new version if a NPM next pre-release is cut. */ - private _newVersion: Promise = this._computeNewVersion(); - - override async getDescription() { - const {branchName} = this._getActivePrereleaseTrain(); - const newVersion = await this._newVersion; - return `Cut a new next pre-release for the "${branchName}" branch (v${newVersion}).`; - } - - override async perform() { - const releaseTrain = this._getActivePrereleaseTrain(); - const {branchName} = releaseTrain; - const newVersion = await this._newVersion; - - const {pullRequest, releaseNotes} = - await this.checkoutBranchAndStageVersion(newVersion, branchName); - - await this.waitForPullRequestToBeMerged(pullRequest); - await this.buildAndPublish(releaseNotes, branchName, 'next'); - - // If the pre-release has been cut from a branch that is not corresponding - // to the next release-train, cherry-pick the changelog into the primary - // development branch. i.e. the `next` branch that is usually `master`. - if (releaseTrain !== this.active.next) { - await this.cherryPickChangelogIntoNextBranch(releaseNotes, branchName); - } - } - - /** Gets the release train for which NPM next pre-releases should be cut. */ - private _getActivePrereleaseTrain(): ReleaseTrain { - return this.active.releaseCandidate ?? this.active.next; - } - - /** Gets the new pre-release version for this release action. */ - private async _computeNewVersion(): Promise { - const releaseTrain = this._getActivePrereleaseTrain(); - // If a pre-release is cut for the next release-train, the new version is computed - // with respect to special cases surfacing with FF/RC branches. Otherwise, the basic - // pre-release increment of the version is used as new version. - if (releaseTrain === this.active.next) { - return await computeNewPrereleaseVersionForNext(this.active, this.config); - } else { - return semverInc(releaseTrain.version, 'prerelease'); - } - } - - static override async isActive() { - // Pre-releases for the `next` NPM dist tag can always be cut. Depending on whether - // there is a feature-freeze/release-candidate branch, the next pre-releases are either - // cut from such a branch, or from the actual `next` release-train branch (i.e. master). - return true; - } -} diff --git a/dev-infra/release/publish/actions/cut-release-candidate-for-feature-freeze.ts b/dev-infra/release/publish/actions/cut-release-candidate-for-feature-freeze.ts deleted file mode 100644 index bf3d8e6c8aa4d3..00000000000000 --- a/dev-infra/release/publish/actions/cut-release-candidate-for-feature-freeze.ts +++ /dev/null @@ -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 - */ - -import {semverInc} from '../../../utils/semver'; -import {ActiveReleaseTrains} from '../../versioning/active-release-trains'; -import {ReleaseAction} from '../actions'; - -/** - * Cuts the first release candidate for a release-train currently in the - * feature-freeze phase. The version is bumped from `next` to `rc.0`. - */ -export class CutReleaseCandidateForFeatureFreezeAction extends ReleaseAction { - private _newVersion = semverInc(this.active.releaseCandidate!.version, 'prerelease', 'rc'); - - override async getDescription() { - const newVersion = this._newVersion; - return `Cut a first release-candidate for the feature-freeze branch (v${newVersion}).`; - } - - override async perform() { - const {branchName} = this.active.releaseCandidate!; - const newVersion = this._newVersion; - - const {pullRequest, releaseNotes} = - await this.checkoutBranchAndStageVersion(newVersion, branchName); - - await this.waitForPullRequestToBeMerged(pullRequest); - await this.buildAndPublish(releaseNotes, branchName, 'next'); - await this.cherryPickChangelogIntoNextBranch(releaseNotes, branchName); - } - - static override async isActive(active: ActiveReleaseTrains) { - // A release-candidate can be cut for an active release-train currently - // in the feature-freeze phase. - return active.releaseCandidate !== null && - active.releaseCandidate.version.prerelease[0] === 'next'; - } -} diff --git a/dev-infra/release/publish/actions/cut-stable.ts b/dev-infra/release/publish/actions/cut-stable.ts deleted file mode 100644 index 7c8747a8dba630..00000000000000 --- a/dev-infra/release/publish/actions/cut-stable.ts +++ /dev/null @@ -1,83 +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 semver from 'semver'; - -import {ActiveReleaseTrains} from '../../versioning/active-release-trains'; -import {getLtsNpmDistTagOfMajor} from '../../versioning/long-term-support'; -import {ReleaseAction} from '../actions'; -import {invokeSetNpmDistCommand, invokeYarnInstallCommand} from '../external-commands'; - -/** - * Release action that cuts a stable version for the current release-train in the release - * candidate phase. The pre-release release-candidate version label is removed. - */ -export class CutStableAction extends ReleaseAction { - private _newVersion = this._computeNewVersion(); - - override async getDescription() { - const newVersion = this._newVersion; - return `Cut a stable release for the release-candidate branch (v${newVersion}).`; - } - - override async perform() { - const {branchName} = this.active.releaseCandidate!; - const newVersion = this._newVersion; - const isNewMajor = this.active.releaseCandidate?.isMajor; - - const {pullRequest, releaseNotes} = - await this.checkoutBranchAndStageVersion(newVersion, branchName); - - await this.waitForPullRequestToBeMerged(pullRequest); - - // If a new major version is published, we publish to the `next` NPM dist tag temporarily. - // We do this because for major versions, we want all main Angular projects to have their - // new major become available at the same time. Publishing immediately to the `latest` NPM - // dist tag could cause inconsistent versions when users install packages with `@latest`. - // For example: Consider Angular Framework releases v12. CLI and Components would need to - // wait for that release to complete. Once done, they can update their dependencies to point - // to v12. Afterwards they could start the release process. In the meanwhile though, the FW - // dependencies were already available as `@latest`, so users could end up installing v12 while - // still having the older (but currently still latest) CLI version that is incompatible. - // The major release can be re-tagged to `latest` through a separate release action. - await this.buildAndPublish(releaseNotes, branchName, isNewMajor ? 'next' : 'latest'); - - // If a new major version is published and becomes the "latest" release-train, we need - // to set the LTS npm dist tag for the previous latest release-train (the current patch). - if (isNewMajor) { - const previousPatch = this.active.latest; - const ltsTagForPatch = getLtsNpmDistTagOfMajor(previousPatch.version.major); - - // Instead of directly setting the NPM dist tags, we invoke the ng-dev command for - // setting the NPM dist tag to the specified version. We do this because release NPM - // packages could be different in the previous patch branch, and we want to set the - // LTS tag for all packages part of the last major. It would not be possible to set the - // NPM dist tag for new packages part of the released major, nor would it be acceptable - // to skip the LTS tag for packages which are no longer part of the new major. - await this.checkoutUpstreamBranch(previousPatch.branchName); - await invokeYarnInstallCommand(this.projectDir); - await invokeSetNpmDistCommand(ltsTagForPatch, previousPatch.version); - } - - await this.cherryPickChangelogIntoNextBranch(releaseNotes, branchName); - } - - /** Gets the new stable version of the release candidate release-train. */ - private _computeNewVersion(): semver.SemVer { - const {version} = this.active.releaseCandidate!; - return semver.parse(`${version.major}.${version.minor}.${version.patch}`)!; - } - - static override async isActive(active: ActiveReleaseTrains) { - // A stable version can be cut for an active release-train currently in the - // release-candidate phase. Note: It is not possible to directly release from - // feature-freeze phase into a stable version. - return active.releaseCandidate !== null && - active.releaseCandidate.version.prerelease[0] === 'rc'; - } -} diff --git a/dev-infra/release/publish/actions/index.ts b/dev-infra/release/publish/actions/index.ts deleted file mode 100644 index d3f7558591e7df..00000000000000 --- a/dev-infra/release/publish/actions/index.ts +++ /dev/null @@ -1,33 +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 {ReleaseActionConstructor} from '../actions'; - -import {CutLongTermSupportPatchAction} from './cut-lts-patch'; -import {CutNewPatchAction} from './cut-new-patch'; -import {CutNextPrereleaseAction} from './cut-next-prerelease'; -import {CutReleaseCandidateForFeatureFreezeAction} from './cut-release-candidate-for-feature-freeze'; -import {CutStableAction} from './cut-stable'; -import {MoveNextIntoFeatureFreezeAction} from './move-next-into-feature-freeze'; -import {MoveNextIntoReleaseCandidateAction} from './move-next-into-release-candidate'; -import {TagRecentMajorAsLatest} from './tag-recent-major-as-latest'; - -/** - * List of release actions supported by the release staging tool. These are sorted - * by priority. Actions which are selectable are sorted based on this declaration order. - */ -export const actions: ReleaseActionConstructor[] = [ - TagRecentMajorAsLatest, - CutStableAction, - CutReleaseCandidateForFeatureFreezeAction, - CutNewPatchAction, - CutNextPrereleaseAction, - MoveNextIntoFeatureFreezeAction, - MoveNextIntoReleaseCandidateAction, - CutLongTermSupportPatchAction, -]; diff --git a/dev-infra/release/publish/actions/move-next-into-feature-freeze.ts b/dev-infra/release/publish/actions/move-next-into-feature-freeze.ts deleted file mode 100644 index 90ccd3d8270b10..00000000000000 --- a/dev-infra/release/publish/actions/move-next-into-feature-freeze.ts +++ /dev/null @@ -1,28 +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 {ActiveReleaseTrains} from '../../versioning'; - -import {BranchOffNextBranchBaseAction} from './branch-off-next-branch'; - -/** - * Release action that moves the next release-train into the feature-freeze phase. This means - * that a new version branch is created from the next branch, and a new next pre-release is - * cut indicating the started feature-freeze. - */ -export class MoveNextIntoFeatureFreezeAction extends BranchOffNextBranchBaseAction { - override newPhaseName = 'feature-freeze' as const; - - static override async isActive(active: ActiveReleaseTrains) { - // A new feature-freeze branch can only be created if there is no active - // release-train in feature-freeze/release-candidate phase and the version - // currently in the `next` branch is for a major. The feature-freeze phase - // is not foreseen for minor versions. - return active.releaseCandidate === null && active.next.isMajor; - } -} diff --git a/dev-infra/release/publish/actions/move-next-into-release-candidate.ts b/dev-infra/release/publish/actions/move-next-into-release-candidate.ts deleted file mode 100644 index 4d9b7280edb91a..00000000000000 --- a/dev-infra/release/publish/actions/move-next-into-release-candidate.ts +++ /dev/null @@ -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 {ActiveReleaseTrains} from '../../versioning'; - -import {BranchOffNextBranchBaseAction} from './branch-off-next-branch'; - -/** - * Release action that moves the next release-train into the release-candidate phase. This means - * that a new version branch is created from the next branch, and the first release candidate - * version is cut indicating the new phase. - */ -export class MoveNextIntoReleaseCandidateAction extends BranchOffNextBranchBaseAction { - override newPhaseName = 'release-candidate' as const; - - static override async isActive(active: ActiveReleaseTrains) { - // Directly switching a next release-train into the `release-candidate` - // phase is only allowed for minor releases. Major version always need to - // go through the `feature-freeze` phase. - return active.releaseCandidate === null && !active.next.isMajor; - } -} diff --git a/dev-infra/release/publish/actions/tag-recent-major-as-latest.ts b/dev-infra/release/publish/actions/tag-recent-major-as-latest.ts deleted file mode 100644 index 004441cfb42d2f..00000000000000 --- a/dev-infra/release/publish/actions/tag-recent-major-as-latest.ts +++ /dev/null @@ -1,53 +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 semver from 'semver'; - -import {ReleaseConfig} from '../../config'; -import {ActiveReleaseTrains} from '../../versioning/active-release-trains'; -import {fetchProjectNpmPackageInfo} from '../../versioning/npm-registry'; -import {ReleaseAction} from '../actions'; -import {invokeSetNpmDistCommand, invokeYarnInstallCommand} from '../external-commands'; - -/** - * Release action that tags the recently published major as latest within the NPM - * registry. Major versions are published to the `next` NPM dist tag initially and - * can be re-tagged to the `latest` NPM dist tag. This allows caretakers to make major - * releases available at the same time. e.g. Framework, Tooling and Components - * are able to publish v12 to `@latest` at the same time. This wouldn't be possible if - * we directly publish to `@latest` because Tooling and Components needs to wait - * for the major framework release to be available on NPM. - * @see {CutStableAction#perform} for more details. - */ -export class TagRecentMajorAsLatest extends ReleaseAction { - override async getDescription() { - return `Tag recently published major v${this.active.latest.version} as "next" in NPM.`; - } - - override async perform() { - await this.checkoutUpstreamBranch(this.active.latest.branchName); - await invokeYarnInstallCommand(this.projectDir); - await invokeSetNpmDistCommand('latest', this.active.latest.version); - } - - static override async isActive({latest}: ActiveReleaseTrains, config: ReleaseConfig) { - // If the latest release-train does currently not have a major version as version. e.g. - // the latest branch is `10.0.x` with the version being `10.0.2`. In such cases, a major - // has not been released recently, and this action should never become active. - if (latest.version.minor !== 0 || latest.version.patch !== 0) { - return false; - } - - const packageInfo = await fetchProjectNpmPackageInfo(config); - const npmLatestVersion = semver.parse(packageInfo['dist-tags']['latest']); - // This action only becomes active if a major just has been released recently, but is - // not set to the `latest` NPM dist tag in the NPM registry. Note that we only allow - // re-tagging if the current `@latest` in NPM is the previous major version. - return npmLatestVersion !== null && npmLatestVersion.major === latest.version.major - 1; - } -} diff --git a/dev-infra/release/publish/cli.ts b/dev-infra/release/publish/cli.ts deleted file mode 100644 index 5a6ec8363d2032..00000000000000 --- a/dev-infra/release/publish/cli.ts +++ /dev/null @@ -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 {Arguments, Argv, CommandModule} from 'yargs'; - -import {getConfig} from '../../utils/config'; -import {error, green, info, red, yellow} from '../../utils/console'; -import {GitClient} from '../../utils/git/git-client'; -import {addGithubTokenOption} from '../../utils/git/github-yargs'; -import {getReleaseConfig} from '../config/index'; - -import {CompletionState, ReleaseTool} from './index'; - -/** Command line options for publishing a release. */ -export interface ReleasePublishOptions { - githubToken: string; -} - -/** Yargs command builder for configuring the `ng-dev release publish` command. */ -function builder(argv: Argv): Argv { - return addGithubTokenOption(argv); -} - -/** Yargs command handler for staging a release. */ -async function handler() { - const git = GitClient.get(); - const config = getConfig(); - const releaseConfig = getReleaseConfig(config); - const projectDir = git.baseDir; - const task = new ReleaseTool(releaseConfig, config.github, projectDir); - const result = await task.run(); - - switch (result) { - case CompletionState.FATAL_ERROR: - error(red(`Release action has been aborted due to fatal errors. See above.`)); - process.exitCode = 2; - break; - case CompletionState.MANUALLY_ABORTED: - info(yellow(`Release action has been manually aborted.`)); - process.exitCode = 1; - break; - case CompletionState.SUCCESS: - info(green(`Release action has completed successfully.`)); - break; - } -} - -/** CLI command module for publishing a release. */ -export const ReleasePublishCommandModule: CommandModule<{}, ReleasePublishOptions> = { - builder, - handler, - command: 'publish', - describe: 'Publish new releases and configure version branches.', -}; diff --git a/dev-infra/release/publish/commit-message.ts b/dev-infra/release/publish/commit-message.ts deleted file mode 100644 index a4bc73aa2c3cb6..00000000000000 --- a/dev-infra/release/publish/commit-message.ts +++ /dev/null @@ -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 * as semver from 'semver'; - -/** Gets the commit message for a new release point in the project. */ -export function getCommitMessageForRelease(newVersion: semver.SemVer): string { - return `release: cut the v${newVersion} release`; -} - -/** - * Gets the commit message for an exceptional version bump in the next branch. The next - * branch version will be bumped without the release being published in some situations. - * More details can be found in the `MoveNextIntoFeatureFreeze` release action and in: - * https://hackmd.io/2Le8leq0S6G_R5VEVTNK9A. - */ -export function getCommitMessageForExceptionalNextVersionBump(newVersion: semver.SemVer) { - return `release: bump the next branch to v${newVersion}`; -} - -/** - * Gets the commit message for a version update in the next branch to a major version. The next - * branch version will be updated without the release being published if the branch is configured - * as a major. More details can be found in the `ConfigureNextAsMajor` release action and in: - * https://hackmd.io/2Le8leq0S6G_R5VEVTNK9A. - */ -export function getCommitMessageForNextBranchMajorSwitch(newVersion: semver.SemVer) { - return `release: switch the next branch to v${newVersion}`; -} - -/** Gets the commit message for a release notes cherry-pick commit */ -export function getReleaseNoteCherryPickCommitMessage(newVersion: semver.SemVer): string { - return `docs: release notes for the v${newVersion} release`; -} diff --git a/dev-infra/release/publish/constants.ts b/dev-infra/release/publish/constants.ts deleted file mode 100644 index 530c1b8c47f083..00000000000000 --- a/dev-infra/release/publish/constants.ts +++ /dev/null @@ -1,16 +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 - */ - -/** Project-relative path for the "package.json" file. */ -export const packageJsonPath = 'package.json'; - -/** Project-relative path for the changelog file. */ -export const changelogPath = 'CHANGELOG.md'; - -/** Default interval in milliseconds to check whether a pull request has been merged. */ -export const waitForPullRequestInterval = 10000; diff --git a/dev-infra/release/publish/external-commands.ts b/dev-infra/release/publish/external-commands.ts deleted file mode 100644 index f0b9edf3665035..00000000000000 --- a/dev-infra/release/publish/external-commands.ts +++ /dev/null @@ -1,92 +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 ora from 'ora'; -import * as semver from 'semver'; - -import {spawn} from '../../utils/child-process'; -import {error, green, info, red} from '../../utils/console'; -import {BuiltPackage} from '../config/index'; -import {NpmDistTag} from '../versioning'; - -import {FatalReleaseActionError} from './actions-error'; - -/* - * ############################################################### - * - * This file contains helpers for invoking external `ng-dev` commands. A subset of actions, - * like building release output or setting aν NPM dist tag for release packages, cannot be - * performed directly as part of the release tool and need to be delegated to external `ng-dev` - * commands that exist across arbitrary version branches. - * - * In a concrete example: Consider a new patch version is released and that a new release - * package has been added to the `next` branch. The patch branch will not contain the new - * release package, so we could not build the release output for it. To work around this, we - * call the ng-dev build command for the patch version branch and expect it to return a list - * of built packages that need to be released as part of this release train. - * - * ############################################################### - */ - -/** - * Invokes the `ng-dev release set-dist-tag` command in order to set the specified - * NPM dist tag for all packages in the checked out branch to the given version. - */ -export async function invokeSetNpmDistCommand(npmDistTag: NpmDistTag, version: semver.SemVer) { - try { - // Note: No progress indicator needed as that is the responsibility of the command. - await spawn( - 'yarn', ['--silent', 'ng-dev', 'release', 'set-dist-tag', npmDistTag, version.format()]); - info(green(` ✓ Set "${npmDistTag}" NPM dist tag for all packages to v${version}.`)); - } catch (e) { - error(e); - error(red(` ✘ An error occurred while setting the NPM dist tag for "${npmDistTag}".`)); - throw new FatalReleaseActionError(); - } -} - -/** - * Invokes the `ng-dev release build` command in order to build the release - * packages for the currently checked out branch. - */ -export async function invokeReleaseBuildCommand(): Promise { - const spinner = ora.call(undefined).start('Building release output.'); - try { - // Since we expect JSON to be printed from the `ng-dev release build` command, - // we spawn the process in silent mode. We have set up an Ora progress spinner. - const {stdout} = - await spawn('yarn', ['--silent', 'ng-dev', 'release', 'build', '--json'], {mode: 'silent'}); - spinner.stop(); - info(green(' ✓ Built release output for all packages.')); - // The `ng-dev release build` command prints a JSON array to stdout - // that represents the built release packages and their output paths. - return JSON.parse(stdout.trim()) as BuiltPackage[]; - } catch (e) { - spinner.stop(); - error(e); - error(red(' ✘ An error occurred while building the release packages.')); - throw new FatalReleaseActionError(); - } -} - -/** - * Invokes the `yarn install` command in order to install dependencies for - * the configured project with the currently checked out revision. - */ -export async function invokeYarnInstallCommand(projectDir: string): Promise { - try { - // Note: No progress indicator needed as that is the responsibility of the command. - // TODO: Consider using an Ora spinner instead to ensure minimal console output. - await spawn('yarn', ['install', '--frozen-lockfile', '--non-interactive'], {cwd: projectDir}); - info(green(' ✓ Installed project dependencies.')); - } catch (e) { - error(e); - error(red(' ✘ An error occurred while installing dependencies.')); - throw new FatalReleaseActionError(); - } -} diff --git a/dev-infra/release/publish/graphql-queries.ts b/dev-infra/release/publish/graphql-queries.ts deleted file mode 100644 index 63679d7a77bd5a..00000000000000 --- a/dev-infra/release/publish/graphql-queries.ts +++ /dev/null @@ -1,31 +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 {params, types} from 'typed-graphqlify'; - -/** - * Graphql Github API query that can be used to find forks of a given repository - * that are owned by the current viewer authenticated with the Github API. - */ -export const findOwnedForksOfRepoQuery = params( - { - $owner: 'String!', - $name: 'String!', - }, - { - repository: params({owner: '$owner', name: '$name'}, { - forks: params({affiliations: 'OWNER', first: 1}, { - nodes: [{ - owner: { - login: types.string, - }, - name: types.string, - }], - }), - }), - }); diff --git a/dev-infra/release/publish/index.ts b/dev-infra/release/publish/index.ts deleted file mode 100644 index f5c551a2d02c6e..00000000000000 --- a/dev-infra/release/publish/index.ts +++ /dev/null @@ -1,214 +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 {ListChoiceOptions, prompt} from 'inquirer'; - -import {spawn} from '../../utils/child-process'; -import {GithubConfig} from '../../utils/config'; -import {debug, error, info, log, promptConfirm, red, yellow} from '../../utils/console'; -import {AuthenticatedGitClient} from '../../utils/git/authenticated-git-client'; -import {ReleaseConfig} from '../config/index'; -import {ActiveReleaseTrains, fetchActiveReleaseTrains, nextBranchName} from '../versioning/active-release-trains'; -import {npmIsLoggedIn, npmLogin, npmLogout} from '../versioning/npm-publish'; -import {printActiveReleaseTrains} from '../versioning/print-active-trains'; -import {GithubRepoWithApi} from '../versioning/version-branches'; - -import {ReleaseAction} from './actions'; -import {FatalReleaseActionError, UserAbortedReleaseActionError} from './actions-error'; -import {actions} from './actions/index'; - -export enum CompletionState { - SUCCESS, - FATAL_ERROR, - MANUALLY_ABORTED, -} - -export class ReleaseTool { - /** The singleton instance of the authenticated git client. */ - private _git = AuthenticatedGitClient.get(); - /** The previous git commit to return back to after the release tool runs. */ - private previousGitBranchOrRevision = this._git.getCurrentBranchOrRevision(); - - constructor( - protected _config: ReleaseConfig, protected _github: GithubConfig, - protected _projectRoot: string) {} - - /** Runs the interactive release tool. */ - async run(): Promise { - log(); - log(yellow('--------------------------------------------')); - log(yellow(' Angular Dev-Infra release staging script')); - log(yellow('--------------------------------------------')); - log(); - - if (!await this._verifyEnvironmentHasPython3Symlink() || - !await this._verifyNoUncommittedChanges() || !await this._verifyRunningFromNextBranch()) { - return CompletionState.FATAL_ERROR; - } - - if (!await this._verifyNpmLoginState()) { - return CompletionState.MANUALLY_ABORTED; - } - - const {owner, name} = this._github; - const repo: GithubRepoWithApi = {owner, name, api: this._git.github}; - const releaseTrains = await fetchActiveReleaseTrains(repo); - - // Print the active release trains so that the caretaker can access - // the current project branching state without switching context. - await printActiveReleaseTrains(releaseTrains, this._config); - - const action = await this._promptForReleaseAction(releaseTrains); - - try { - await action.perform(); - } catch (e) { - if (e instanceof UserAbortedReleaseActionError) { - return CompletionState.MANUALLY_ABORTED; - } - // Only print the error message and stack if the error is not a known fatal release - // action error (for which we print the error gracefully to the console with colors). - if (!(e instanceof FatalReleaseActionError) && e instanceof Error) { - console.error(e); - } - return CompletionState.FATAL_ERROR; - } finally { - await this.cleanup(); - } - - return CompletionState.SUCCESS; - } - - /** Run post release tool cleanups. */ - private async cleanup(): Promise { - // Return back to the git state from before the release tool ran. - this._git.checkout(this.previousGitBranchOrRevision, true); - // Ensure log out of NPM. - await npmLogout(this._config.publishRegistry); - } - - /** Prompts the caretaker for a release action that should be performed. */ - private async _promptForReleaseAction(activeTrains: ActiveReleaseTrains) { - const choices: ListChoiceOptions[] = []; - - // Find and instantiate all release actions which are currently valid. - for (let actionType of actions) { - if (await actionType.isActive(activeTrains, this._config)) { - const action: ReleaseAction = - new actionType(activeTrains, this._git, this._config, this._projectRoot); - choices.push({name: await action.getDescription(), value: action}); - } - } - - info('Please select the type of release you want to perform.'); - - const {releaseAction} = await prompt<{releaseAction: ReleaseAction}>({ - name: 'releaseAction', - message: 'Please select an action:', - type: 'list', - choices, - }); - - return releaseAction; - } - - /** - * Verifies that there are no uncommitted changes in the project. - * @returns a boolean indicating success or failure. - */ - private async _verifyNoUncommittedChanges(): Promise { - if (this._git.hasUncommittedChanges()) { - error(red(' ✘ There are changes which are not committed and should be discarded.')); - return false; - } - return true; - } - - /** - * Verifies that Python can be resolved within scripts and points to a compatible version. Python - * is required in Bazel actions as there can be tools (such as `skydoc`) that rely on it. - * @returns a boolean indicating success or failure. - */ - private async _verifyEnvironmentHasPython3Symlink(): Promise { - try { - // Note: We do not rely on `/usr/bin/env` but rather access the `env` binary directly as it - // should be part of the shell's `$PATH`. This is necessary for compatibility with Windows. - const pyVersion = await spawn('env', ['python', '--version'], {mode: 'silent'}); - const version = pyVersion.stdout.trim() || pyVersion.stderr.trim(); - if (version.startsWith('Python 3.')) { - debug(`Local python version: ${version}`); - return true; - } - error(red(` ✘ \`/usr/bin/python\` is currently symlinked to "${version}", please update`)); - error(red(' the symlink to link instead to Python3')); - error(); - error(red(' Googlers: please run the following command to symlink python to python3:')); - error(red(' sudo ln -s /usr/bin/python3 /usr/bin/python')); - return false; - } catch { - error(red(' ✘ `/usr/bin/python` does not exist, please ensure `/usr/bin/python` is')); - error(red(' symlinked to Python3.')); - error(); - error(red(' Googlers: please run the following command to symlink python to python3:')); - error(red(' sudo ln -s /usr/bin/python3 /usr/bin/python')); - } - return false; - } - - /** - * Verifies that the next branch from the configured repository is checked out. - * @returns a boolean indicating success or failure. - */ - private async _verifyRunningFromNextBranch(): Promise { - const headSha = this._git.run(['rev-parse', 'HEAD']).stdout.trim(); - const {data} = - await this._git.github.repos.getBranch({...this._git.remoteParams, branch: nextBranchName}); - - if (headSha !== data.commit.sha) { - error(red(' ✘ Running release tool from an outdated local branch.')); - error(red(` Please make sure you are running from the "${nextBranchName}" branch.`)); - return false; - } - return true; - } - - /** - * Verifies that the user is logged into NPM at the correct registry, if defined for the release. - * @returns a boolean indicating whether the user is logged into NPM. - */ - private async _verifyNpmLoginState(): Promise { - const registry = `NPM at the ${this._config.publishRegistry ?? 'default NPM'} registry`; - // TODO(josephperrott): remove wombat specific block once wombot allows `npm whoami` check to - // check the status of the local token in the .npmrc file. - if (this._config.publishRegistry?.includes('wombat-dressing-room.appspot.com')) { - info('Unable to determine NPM login state for wombat proxy, requiring login now.'); - try { - await npmLogin(this._config.publishRegistry); - } catch { - return false; - } - return true; - } - if (await npmIsLoggedIn(this._config.publishRegistry)) { - debug(`Already logged into ${registry}.`); - return true; - } - error(red(` ✘ Not currently logged into ${registry}.`)); - const shouldLogin = await promptConfirm('Would you like to log into NPM now?'); - if (shouldLogin) { - debug('Starting NPM login.'); - try { - await npmLogin(this._config.publishRegistry); - } catch { - return false; - } - return true; - } - return false; - } -} diff --git a/dev-infra/release/publish/pull-request-state.ts b/dev-infra/release/publish/pull-request-state.ts deleted file mode 100644 index d74eb21806a757..00000000000000 --- a/dev-infra/release/publish/pull-request-state.ts +++ /dev/null @@ -1,79 +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'; - -/** Thirty seconds in milliseconds. */ -const THIRTY_SECONDS_IN_MS = 30000; - -/** State of a pull request in Github. */ -export type PullRequestState = 'merged'|'closed'|'open'; - -/** Gets whether a given pull request has been merged. */ -export async function getPullRequestState(api: GitClient, id: number): Promise { - const {data} = await api.github.pulls.get({...api.remoteParams, pull_number: id}); - if (data.merged) { - return 'merged'; - } - // Check if the PR was closed more than 30 seconds ago, this extra time gives Github time to - // update the closed pull request to be associated with the closing commit. - // Note: a Date constructed with `null` creates an object at 0 time, which will never be greater - // than the current date time. - if (data.closed_at !== null && - (new Date(data.closed_at).getTime() < Date.now() - THIRTY_SECONDS_IN_MS)) { - return await isPullRequestClosedWithAssociatedCommit(api, id) ? 'merged' : 'closed'; - } - return 'open'; -} - -/** - * Whether the pull request has been closed with an associated commit. This is usually - * the case if a PR has been merged using the autosquash merge script strategy. Since - * the merge is not fast-forward, Github does not consider the PR as merged and instead - * shows the PR as closed. See for example: https://github.com/angular/angular/pull/37918. - */ -async function isPullRequestClosedWithAssociatedCommit(api: GitClient, id: number) { - const events = await api.github.paginate( - api.github.issues.listEvents, {...api.remoteParams, issue_number: id}); - // Iterate through the events of the pull request in reverse. We want to find the most - // recent events and check if the PR has been closed with a commit associated with it. - // If the PR has been closed through a commit, we assume that the PR has been merged - // using the autosquash merge strategy. For more details. See the `AutosquashMergeStrategy`. - for (let i = events.length - 1; i >= 0; i--) { - const {event, commit_id} = events[i]; - // If we come across a "reopened" event, we abort looking for referenced commits. Any - // commits that closed the PR before, are no longer relevant and did not close the PR. - if (event === 'reopened') { - return false; - } - // If a `closed` event is captured with a commit assigned, then we assume that - // this PR has been merged properly. - if (event === 'closed' && commit_id) { - return true; - } - // If the PR has been referenced by a commit, check if the commit closes this pull - // request. Note that this is needed besides checking `closed` as PRs could be merged - // into any non-default branch where the `Closes <..>` keyword does not work and the PR - // is simply closed without an associated `commit_id`. For more details see: - // https://docs.github.com/en/enterprise/2.16/user/github/managing-your-work-on-github/closing-issues-using-keywords#:~:text=non-default. - if (event === 'referenced' && commit_id && - await isCommitClosingPullRequest(api, commit_id, id)) { - return true; - } - } - return false; -} - -/** Checks whether the specified commit is closing the given pull request. */ -async function isCommitClosingPullRequest(api: GitClient, sha: string, id: number) { - const {data} = await api.github.repos.getCommit({...api.remoteParams, ref: sha}); - // Matches the closing keyword supported in commit messages. See: - // https://docs.github.com/en/enterprise/2.16/user/github/managing-your-work-on-github/closing-issues-using-keywords. - return data.commit.message.match( - new RegExp(`(?:close[sd]?|fix(?:e[sd]?)|resolve[sd]?):? #${id}(?!\\d)`, 'i')); -} diff --git a/dev-infra/release/publish/test/BUILD.bazel b/dev-infra/release/publish/test/BUILD.bazel deleted file mode 100644 index b27935084d9c47..00000000000000 --- a/dev-infra/release/publish/test/BUILD.bazel +++ /dev/null @@ -1,36 +0,0 @@ -load("//dev-infra:defaults.bzl", "jasmine_node_test", "ts_library") - -ts_library( - name = "test_lib", - srcs = glob([ - "**/*.ts", - ]), - deps = [ - "//dev-infra/commit-message", - "//dev-infra/release/config", - "//dev-infra/release/notes", - "//dev-infra/release/publish", - "//dev-infra/release/versioning", - "//dev-infra/utils", - "//dev-infra/utils/testing", - "@npm//@types/jasmine", - "@npm//@types/minimist", - "@npm//@types/node", - "@npm//@types/node-fetch", - "@npm//@types/semver", - "@npm//minimist", - "@npm//nock", - "@npm//node-fetch", - "@npm//semver", - ], -) - -jasmine_node_test( - name = "test", - # Disable the Bazel patched module resolution. It always loads ".mjs" files first. This - # breaks NodeJS execution for "node-fetch" as it uses experimental modules which are not - # enabled in NodeJS. TODO: Remove this with rules_nodejs 3.x where patching is optional. - # https://github.com/bazelbuild/rules_nodejs/commit/7d070ffadf9c3b41711382a4737b995f987c14fa. - args = ["--nobazel_patch_module_resolver"], - deps = [":test_lib"], -) diff --git a/dev-infra/release/publish/test/branch-off-next-branch-testing.ts b/dev-infra/release/publish/test/branch-off-next-branch-testing.ts deleted file mode 100644 index 1934d872f460c2..00000000000000 --- a/dev-infra/release/publish/test/branch-off-next-branch-testing.ts +++ /dev/null @@ -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 - */ - -import {getBranchPushMatcher} from '../../../utils/testing'; -import {ActiveReleaseTrains} from '../../versioning'; -import * as npm from '../../versioning/npm-publish'; -import {ReleaseActionConstructor} from '../actions'; -import {BranchOffNextBranchBaseAction} from '../actions/branch-off-next-branch'; -import * as externalCommands from '../external-commands'; - -import {setupReleaseActionForTesting, testTmpDir} from './test-utils'; - -/** - * Performs the given branch-off release action and expects versions and - * branches to be determined and created properly. - */ -export async function expectBranchOffActionToRun( - action: ReleaseActionConstructor, active: ActiveReleaseTrains, - isNextPublishedToNpm: boolean, expectedNextVersion: string, expectedVersion: string, - expectedNewBranch: string) { - const {repo, fork, instance, gitClient} = - setupReleaseActionForTesting(action, active, isNextPublishedToNpm); - - const expectedNextUpdateBranch = `next-release-train-${expectedNextVersion}`; - const expectedStagingForkBranch = `release-stage-${expectedVersion}`; - const expectedTagName = expectedVersion; - - // We first mock the commit status check for the next branch, then expect two pull - // requests from a fork that are targeting next and the new feature-freeze branch. - repo.expectBranchRequest('master', 'MASTER_COMMIT_SHA') - .expectCommitStatusCheck('MASTER_COMMIT_SHA', 'success') - .expectFindForkRequest(fork) - .expectPullRequestToBeCreated(expectedNewBranch, fork, expectedStagingForkBranch, 200) - .expectPullRequestWait(200) - .expectBranchRequest(expectedNewBranch, 'STAGING_COMMIT_SHA') - .expectCommitRequest( - 'STAGING_COMMIT_SHA', `release: cut the v${expectedVersion} release\n\nPR Close #200.`) - .expectTagToBeCreated(expectedTagName, 'STAGING_COMMIT_SHA') - .expectReleaseToBeCreated(`v${expectedVersion}`, expectedTagName) - .expectPullRequestToBeCreated('master', fork, expectedNextUpdateBranch, 100); - - // In the fork, we make the following branches appear as non-existent, - // so that the PRs can be created properly without collisions. - fork.expectBranchRequest(expectedStagingForkBranch, null) - .expectBranchRequest(expectedNextUpdateBranch, null); - - await instance.perform(); - - expect(gitClient.pushed.length).toBe(3); - expect(gitClient.pushed[0]) - .toEqual( - getBranchPushMatcher({ - baseRepo: repo, - baseBranch: 'master', - targetRepo: repo, - targetBranch: expectedNewBranch, - expectedCommits: [], - }), - 'Expected new version-branch to be created upstream and based on "master".'); - expect(gitClient.pushed[1]) - .toEqual( - getBranchPushMatcher({ - baseBranch: 'master', - baseRepo: repo, - targetBranch: expectedStagingForkBranch, - targetRepo: fork, - expectedCommits: [{ - message: `release: cut the v${expectedVersion} release`, - files: ['package.json', 'CHANGELOG.md'], - }], - }), - 'Expected release staging branch to be created in fork.'); - - expect(gitClient.pushed[2]) - .toEqual( - getBranchPushMatcher({ - baseBranch: 'master', - baseRepo: repo, - targetBranch: expectedNextUpdateBranch, - targetRepo: fork, - expectedCommits: [ - { - message: `release: bump the next branch to v${expectedNextVersion}`, - files: ['package.json'] - }, - { - message: `docs: release notes for the v${expectedVersion} release`, - files: ['CHANGELOG.md'] - }, - ], - }), - 'Expected next release-train update branch be created in fork.'); - - expect(externalCommands.invokeReleaseBuildCommand).toHaveBeenCalledTimes(1); - expect(npm.runNpmPublish).toHaveBeenCalledTimes(2); - expect(npm.runNpmPublish).toHaveBeenCalledWith(`${testTmpDir}/dist/pkg1`, 'next', undefined); - expect(npm.runNpmPublish).toHaveBeenCalledWith(`${testTmpDir}/dist/pkg2`, 'next', undefined); -} diff --git a/dev-infra/release/publish/test/common.spec.ts b/dev-infra/release/publish/test/common.spec.ts deleted file mode 100644 index e5bba5df635e34..00000000000000 --- a/dev-infra/release/publish/test/common.spec.ts +++ /dev/null @@ -1,163 +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 {join} from 'path'; -import * as semver from 'semver'; - -import {getBranchPushMatcher} from '../../../utils/testing'; -import {ReleaseNotes} from '../../notes/release-notes'; -import {NpmDistTag} from '../../versioning'; -import {ActiveReleaseTrains} from '../../versioning/active-release-trains'; -import * as npm from '../../versioning/npm-publish'; -import {ReleaseTrain} from '../../versioning/release-trains'; -import {ReleaseAction} from '../actions'; -import {actions} from '../actions/index'; -import {changelogPath} from '../constants'; - -import {fakeNpmPackageQueryRequest, getTestingMocksForReleaseAction, parse, setupReleaseActionForTesting, testTmpDir} from './test-utils'; - -describe('common release action logic', () => { - const baseReleaseTrains: ActiveReleaseTrains = { - releaseCandidate: null, - next: new ReleaseTrain('master', parse('10.1.0-next.0')), - latest: new ReleaseTrain('10.0.x', parse('10.0.1')), - }; - - describe('version computation', async () => { - const testReleaseTrain: ActiveReleaseTrains = { - releaseCandidate: new ReleaseTrain('10.1.x', parse('10.1.0-next.3')), - next: new ReleaseTrain('master', parse('10.2.0-next.0')), - latest: new ReleaseTrain('10.0.x', parse('10.0.1')), - }; - - it('should not modify release train versions and cause invalid other actions', async () => { - const {releaseConfig, gitClient} = getTestingMocksForReleaseAction(); - const descriptions: string[] = []; - - // Fake the NPM package request as otherwise the test would rely on `npmjs.org`. - fakeNpmPackageQueryRequest(releaseConfig.npmPackages[0], {'dist-tags': {}}); - - for (const actionCtor of actions) { - if (await actionCtor.isActive(testReleaseTrain, releaseConfig)) { - const action = new actionCtor(testReleaseTrain, gitClient, releaseConfig, testTmpDir); - descriptions.push(await action.getDescription()); - } - } - - expect(descriptions).toEqual([ - `Cut a first release-candidate for the feature-freeze branch (v10.1.0-rc.0).`, - `Cut a new patch release for the "10.0.x" branch (v10.0.2).`, - `Cut a new next pre-release for the "10.1.x" branch (v10.1.0-next.4).`, - `Cut a new release for an active LTS branch (0 active).` - ]); - }); - }); - - describe('build and publishing', () => { - it('should support a custom NPM registry', async () => { - const {repo, instance, releaseConfig} = - setupReleaseActionForTesting(TestAction, baseReleaseTrains); - const {version, branchName} = baseReleaseTrains.next; - const tagName = version.format(); - const customRegistryUrl = 'https://custom-npm-registry.google.com'; - - repo.expectBranchRequest(branchName, 'STAGING_SHA') - .expectCommitRequest('STAGING_SHA', `release: cut the v${version} release`) - .expectTagToBeCreated(tagName, 'STAGING_SHA') - .expectReleaseToBeCreated(`v${version}`, tagName); - - // Set up a custom NPM registry. - releaseConfig.publishRegistry = customRegistryUrl; - - await instance.testBuildAndPublish(version, branchName, 'latest'); - - expect(npm.runNpmPublish).toHaveBeenCalledTimes(2); - expect(npm.runNpmPublish) - .toHaveBeenCalledWith(`${testTmpDir}/dist/pkg1`, 'latest', customRegistryUrl); - expect(npm.runNpmPublish) - .toHaveBeenCalledWith(`${testTmpDir}/dist/pkg2`, 'latest', customRegistryUrl); - }); - }); - - describe('changelog cherry-picking', () => { - const {version, branchName} = baseReleaseTrains.latest; - const forkBranchName = `changelog-cherry-pick-${version}`; - - it('should prepend the changelog to the next branch', async () => { - const {repo, fork, instance, testTmpDir} = - setupReleaseActionForTesting(TestAction, baseReleaseTrains); - - // Expect the changelog to be fetched and return a fake changelog to test that - // it is properly appended. Also expect a pull request to be created in the fork. - repo.expectFindForkRequest(fork) - .expectPullRequestToBeCreated('master', fork, forkBranchName, 200) - .expectPullRequestWait(200); - - // Simulate that the fork branch name is available. - fork.expectBranchRequest(forkBranchName, null); - - await instance.testCherryPickWithPullRequest(version, branchName); - - const changelogContent = readFileSync(join(testTmpDir, changelogPath), 'utf8'); - expect(changelogContent).toEqual(`Changelog Entry for 10.0.1\n\nExisting changelog`); - }); - - it('should push changes to a fork for creating a pull request', async () => { - const {repo, fork, instance, gitClient} = - setupReleaseActionForTesting(TestAction, baseReleaseTrains); - - // Expect the changelog to be fetched and return a fake changelog to test that - // it is properly appended. Also expect a pull request to be created in the fork. - repo.expectFindForkRequest(fork) - .expectPullRequestToBeCreated('master', fork, forkBranchName, 200) - .expectPullRequestWait(200); - - // Simulate that the fork branch name is available. - fork.expectBranchRequest(forkBranchName, null); - - await instance.testCherryPickWithPullRequest(version, branchName); - - expect(gitClient.pushed.length).toBe(1); - expect(gitClient.pushed[0]).toEqual(getBranchPushMatcher({ - targetBranch: forkBranchName, - targetRepo: fork, - baseBranch: 'master', - baseRepo: repo, - expectedCommits: [{ - message: `docs: release notes for the v${version} release`, - files: ['CHANGELOG.md'], - }], - })); - }); - }); -}); - -/** - * Test release action that exposes protected units of the base - * release action class. This allows us to add unit tests. - */ -class TestAction extends ReleaseAction { - override async getDescription() { - return 'Test action'; - } - - override async perform() { - throw Error('Not implemented.'); - } - - async testBuildAndPublish(version: semver.SemVer, publishBranch: string, distTag: NpmDistTag) { - const releaseNotes = await ReleaseNotes.fromRange(version, '', ''); - await this.buildAndPublish(releaseNotes, publishBranch, distTag); - } - - async testCherryPickWithPullRequest(version: semver.SemVer, branch: string) { - const releaseNotes = await ReleaseNotes.fromRange(version, '', ''); - await this.cherryPickChangelogIntoNextBranch(releaseNotes, branch); - } -} diff --git a/dev-infra/release/publish/test/configure-next-as-major.spec.ts b/dev-infra/release/publish/test/configure-next-as-major.spec.ts deleted file mode 100644 index 10fb3b5639b735..00000000000000 --- a/dev-infra/release/publish/test/configure-next-as-major.spec.ts +++ /dev/null @@ -1,79 +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 {getBranchPushMatcher} from '../../../utils/testing'; -import {ReleaseTrain} from '../../versioning/release-trains'; -import {ConfigureNextAsMajorAction} from '../actions/configure-next-as-major'; - -import {parse, setupReleaseActionForTesting} from './test-utils'; - -describe('configure next as major action', () => { - it('should be active if the next branch is for a minor', async () => { - expect(await ConfigureNextAsMajorAction.isActive({ - releaseCandidate: null, - next: new ReleaseTrain('master', parse('10.1.0-next.3')), - latest: new ReleaseTrain('10.0.x', parse('10.0.3')), - })).toBe(true); - }); - - it('should be active regardless of a feature-freeze/release-candidate train', async () => { - expect(await ConfigureNextAsMajorAction.isActive({ - releaseCandidate: new ReleaseTrain('10.1.x', parse('10.1.0-rc.1')), - next: new ReleaseTrain('master', parse('10.2.0-next.3')), - latest: new ReleaseTrain('10.0.x', parse('10.0.3')), - })).toBe(true); - }); - - it('should not be active if the next branch is for a major', async () => { - expect(await ConfigureNextAsMajorAction.isActive({ - releaseCandidate: null, - next: new ReleaseTrain('master', parse('11.0.0-next.0')), - latest: new ReleaseTrain('10.0.x', parse('10.0.3')), - })).toBe(false); - }); - - it('should compute proper version and create staging pull request', async () => { - const action = setupReleaseActionForTesting(ConfigureNextAsMajorAction, { - releaseCandidate: null, - next: new ReleaseTrain('master', parse('10.1.0-next.3')), - latest: new ReleaseTrain('10.0.x', parse('10.0.2')), - }); - - const {repo, fork, gitClient} = action; - const expectedVersion = `11.0.0-next.0`; - const expectedForkBranch = `switch-next-to-major-${expectedVersion}`; - - // We first mock the commit status check for the next branch, then expect two pull - // requests from a fork that are targeting next and the new feature-freeze branch. - repo.expectBranchRequest('master', 'MASTER_COMMIT_SHA') - .expectCommitStatusCheck('MASTER_COMMIT_SHA', 'success') - .expectFindForkRequest(fork) - .expectPullRequestToBeCreated('master', fork, expectedForkBranch, 200); - - // In the fork, we make the staging branch appear as non-existent, - // so that the PR can be created properly without collisions. - fork.expectBranchRequest(expectedForkBranch, null); - - await action.instance.perform(); - - expect(gitClient.pushed.length).toBe(1); - expect(gitClient.pushed[0]) - .toEqual( - getBranchPushMatcher({ - baseBranch: 'master', - baseRepo: repo, - targetBranch: expectedForkBranch, - targetRepo: fork, - expectedCommits: [{ - message: `release: switch the next branch to v${expectedVersion}`, - files: ['package.json'], - }], - }), - 'Expected the update branch to be created in fork for a pull request.'); - }); -}); diff --git a/dev-infra/release/publish/test/cut-lts-patch.spec.ts b/dev-infra/release/publish/test/cut-lts-patch.spec.ts deleted file mode 100644 index d27bbd60883d64..00000000000000 --- a/dev-infra/release/publish/test/cut-lts-patch.spec.ts +++ /dev/null @@ -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 {matchesVersion} from '../../../utils/testing/semver-matchers'; -import {fetchLongTermSupportBranchesFromNpm} from '../../versioning/long-term-support'; -import {ReleaseTrain} from '../../versioning/release-trains'; -import {CutLongTermSupportPatchAction} from '../actions/cut-lts-patch'; - -import {expectStagingAndPublishWithCherryPick, fakeNpmPackageQueryRequest, getTestingMocksForReleaseAction, parse, setupReleaseActionForTesting, testTmpDir} from './test-utils'; - -describe('cut an LTS patch action', () => { - it('should be active', async () => { - expect(await CutLongTermSupportPatchAction.isActive({ - releaseCandidate: null, - next: new ReleaseTrain('master', parse('10.1.0-next.3')), - latest: new ReleaseTrain('10.0.x', parse('10.0.3')), - })).toBe(true); - }); - - it('should be active if there is a feature-freeze train', async () => { - expect(await CutLongTermSupportPatchAction.isActive({ - releaseCandidate: new ReleaseTrain('10.1.x', parse('10.1.0-next.3')), - next: new ReleaseTrain('master', parse('10.2.0-next.3')), - latest: new ReleaseTrain('10.0.x', parse('10.0.3')), - })).toBe(true); - }); - - it('should be active if there is a release-candidate train', async () => { - expect(await CutLongTermSupportPatchAction.isActive({ - releaseCandidate: new ReleaseTrain('10.1.x', parse('10.1.0-rc.0')), - next: new ReleaseTrain('master', parse('10.2.0-next.3')), - latest: new ReleaseTrain('10.0.x', parse('10.0.3')), - })).toBe(true); - }); - - it('should compute proper new version and select correct branch', async () => { - const action = setupReleaseActionForTesting(CutLongTermSupportPatchAction, { - releaseCandidate: null, - next: new ReleaseTrain('master', parse('10.1.0-next.3')), - latest: new ReleaseTrain('10.0.x', parse('10.0.2')), - }); - - spyOn(action.instance, '_promptForTargetLtsBranch') - .and.resolveTo({name: '9.2.x', version: parse('9.2.4'), npmDistTag: 'v9-lts'}); - - await expectStagingAndPublishWithCherryPick(action, '9.2.x', '9.2.5', 'v9-lts'); - }); - - it('should include number of active LTS branches in action description', async () => { - const {releaseConfig, gitClient} = getTestingMocksForReleaseAction(); - const activeReleaseTrains = { - releaseCandidate: null, - next: new ReleaseTrain('master', parse('10.1.0-next.3')), - latest: new ReleaseTrain('10.0.x', parse('10.0.2')), - }; - - fakeNpmPackageQueryRequest(releaseConfig.npmPackages[0], { - 'dist-tags': {'v9-lts': '9.1.2', 'v8-lts': '8.2.2'}, - 'time': { - '9.0.0': new Date().toISOString(), - '8.0.0': new Date().toISOString(), - }, - }); - - const action = new CutLongTermSupportPatchAction( - activeReleaseTrains, gitClient, releaseConfig, testTmpDir); - - expect(await action.getDescription()) - .toEqual(`Cut a new release for an active LTS branch (2 active).`); - }); - - it('should properly determine active and inactive LTS branches', async () => { - const {releaseConfig} = getTestingMocksForReleaseAction(); - fakeNpmPackageQueryRequest(releaseConfig.npmPackages[0], { - 'dist-tags': { - 'v9-lts': '9.2.3', - 'v8-lts': '8.4.4', - 'v7-lts': '7.0.1', - 'v6-lts': '6.0.0', - }, - time: { - '9.0.0': new Date().toISOString(), - '8.0.0': new Date().toISOString(), - // We pick dates for the v6 and v7 major versions that guarantee that the version - // is no longer considered as active LTS version. - '7.0.0': new Date(1912, 5, 23).toISOString(), - '6.0.0': new Date(1912, 5, 23).toISOString(), - }, - }); - - // Note: This accesses a private method, so we need to use an element access to satisfy - // TypeScript. It is acceptable to access the member for fine-grained unit testing due to - // complexity with inquirer we want to avoid. It is not easy to test prompts. - const {active, inactive} = await fetchLongTermSupportBranchesFromNpm(releaseConfig); - - expect(active).toEqual([ - {name: '9.2.x', version: matchesVersion('9.2.3'), npmDistTag: 'v9-lts'}, - {name: '8.4.x', version: matchesVersion('8.4.4'), npmDistTag: 'v8-lts'}, - ]); - expect(inactive).toEqual([ - {name: '7.0.x', version: matchesVersion('7.0.1'), npmDistTag: 'v7-lts'}, - {name: '6.0.x', version: matchesVersion('6.0.0'), npmDistTag: 'v6-lts'}, - ]); - }); -}); diff --git a/dev-infra/release/publish/test/cut-new-patch.spec.ts b/dev-infra/release/publish/test/cut-new-patch.spec.ts deleted file mode 100644 index 9df19191f01cc1..00000000000000 --- a/dev-infra/release/publish/test/cut-new-patch.spec.ts +++ /dev/null @@ -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 {ReleaseTrain} from '../../versioning/release-trains'; -import {CutNewPatchAction} from '../actions/cut-new-patch'; - -import {expectStagingAndPublishWithCherryPick, parse, setupReleaseActionForTesting} from './test-utils'; - -describe('cut new patch action', () => { - it('should be active', async () => { - expect(await CutNewPatchAction.isActive({ - releaseCandidate: null, - next: new ReleaseTrain('master', parse('10.1.0-next.3')), - latest: new ReleaseTrain('10.0.x', parse('10.0.3')), - })).toBe(true); - }); - - it('should compute proper new version and select correct branch', async () => { - const action = setupReleaseActionForTesting(CutNewPatchAction, { - releaseCandidate: null, - next: new ReleaseTrain('master', parse('10.1.0-next.3')), - latest: new ReleaseTrain('10.0.x', parse('10.0.2')), - }); - - await expectStagingAndPublishWithCherryPick(action, '10.0.x', '10.0.3', 'latest'); - }); - - it('should create a proper new version if there is a feature-freeze release-train', async () => { - const action = setupReleaseActionForTesting(CutNewPatchAction, { - releaseCandidate: new ReleaseTrain('10.1.x', parse('10.1.0-next.3')), - next: new ReleaseTrain('master', parse('10.2.0-next.0')), - latest: new ReleaseTrain('10.0.x', parse('10.0.9')), - }); - - await expectStagingAndPublishWithCherryPick(action, '10.0.x', '10.0.10', 'latest'); - }); - - it('should create a proper new version if there is a release-candidate train', async () => { - const action = setupReleaseActionForTesting(CutNewPatchAction, { - releaseCandidate: new ReleaseTrain('10.1.x', parse('10.1.0-rc.0')), - next: new ReleaseTrain('master', parse('10.2.0-next.0')), - latest: new ReleaseTrain('10.0.x', parse('10.0.9')), - }); - - await expectStagingAndPublishWithCherryPick(action, '10.0.x', '10.0.10', 'latest'); - }); -}); diff --git a/dev-infra/release/publish/test/cut-next-prerelease.spec.ts b/dev-infra/release/publish/test/cut-next-prerelease.spec.ts deleted file mode 100644 index 3802d6653eec14..00000000000000 --- a/dev-infra/release/publish/test/cut-next-prerelease.spec.ts +++ /dev/null @@ -1,79 +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 {join} from 'path'; - -import {ReleaseTrain} from '../../versioning/release-trains'; -import {CutNextPrereleaseAction} from '../actions/cut-next-prerelease'; -import {packageJsonPath} from '../constants'; - -import {expectStagingAndPublishWithCherryPick, expectStagingAndPublishWithoutCherryPick, parse, setupReleaseActionForTesting} from './test-utils'; - -describe('cut next pre-release action', () => { - it('should always be active regardless of release-trains', async () => { - expect(await CutNextPrereleaseAction.isActive()).toBe(true); - }); - - it('should cut a pre-release for the next branch if there is no FF/RC branch', async () => { - const action = setupReleaseActionForTesting(CutNextPrereleaseAction, { - releaseCandidate: null, - next: new ReleaseTrain('master', parse('10.2.0-next.0')), - latest: new ReleaseTrain('10.1.x', parse('10.1.2')), - }); - - await expectStagingAndPublishWithoutCherryPick(action, 'master', '10.2.0-next.1', 'next'); - }); - - // This is test for a special case in the release tooling. Whenever we branch off for - // feature-freeze, we immediately bump the version in the `next` branch but do not publish - // it. This is because there are no new changes in the next branch that wouldn't be part of - // the branched-off feature-freeze release-train. Also while a FF/RC is active, we cannot - // publish versions to the NPM dist tag. This means that the version is later published, but - // still needs all the staging work (e.g. changelog). We special-case this by not incrementing - // the version if the version in the next branch has not been published yet. - it('should not bump version if current next version has not been published', async () => { - const action = setupReleaseActionForTesting( - CutNextPrereleaseAction, { - releaseCandidate: null, - next: new ReleaseTrain('master', parse('10.2.0-next.0')), - latest: new ReleaseTrain('10.1.x', parse('10.1.0')), - }, - /* isNextPublishedToNpm */ false); - - await expectStagingAndPublishWithoutCherryPick(action, 'master', '10.2.0-next.0', 'next'); - - const pkgJsonContents = readFileSync(join(action.testTmpDir, packageJsonPath), 'utf8'); - const pkgJson = JSON.parse(pkgJsonContents) as {version: string, [key: string]: any}; - expect(pkgJson.version).toBe('10.2.0-next.0', 'Expected version to not have changed.'); - }); - - describe('with active feature-freeze', () => { - it('should create a proper new version and select correct branch', async () => { - const action = setupReleaseActionForTesting(CutNextPrereleaseAction, { - releaseCandidate: new ReleaseTrain('10.1.x', parse('10.1.0-next.4')), - next: new ReleaseTrain('master', parse('10.2.0-next.0')), - latest: new ReleaseTrain('10.0.x', parse('10.0.2')), - }); - - await expectStagingAndPublishWithCherryPick(action, '10.1.x', '10.1.0-next.5', 'next'); - }); - }); - - describe('with active release-candidate', () => { - it('should create a proper new version and select correct branch', async () => { - const action = setupReleaseActionForTesting(CutNextPrereleaseAction, { - releaseCandidate: new ReleaseTrain('10.1.x', parse('10.1.0-rc.0')), - next: new ReleaseTrain('master', parse('10.2.0-next.0')), - latest: new ReleaseTrain('10.0.x', parse('10.0.2')), - }); - - await expectStagingAndPublishWithCherryPick(action, '10.1.x', '10.1.0-rc.1', 'next'); - }); - }); -}); diff --git a/dev-infra/release/publish/test/cut-release-candidate-for-feature-freeze.spec.ts b/dev-infra/release/publish/test/cut-release-candidate-for-feature-freeze.spec.ts deleted file mode 100644 index abb913f0903eb1..00000000000000 --- a/dev-infra/release/publish/test/cut-release-candidate-for-feature-freeze.spec.ts +++ /dev/null @@ -1,49 +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 {ReleaseTrain} from '../../versioning/release-trains'; -import {CutReleaseCandidateForFeatureFreezeAction} from '../actions/cut-release-candidate-for-feature-freeze'; - -import {expectStagingAndPublishWithCherryPick, parse, setupReleaseActionForTesting} from './test-utils'; - -describe('cut release candidate for feature-freeze action', () => { - it('should activate if a feature-freeze release-train is active', async () => { - expect(await CutReleaseCandidateForFeatureFreezeAction.isActive({ - releaseCandidate: new ReleaseTrain('10.1.x', parse('10.1.0-next.1')), - next: new ReleaseTrain('master', parse('10.2.0-next.0')), - latest: new ReleaseTrain('10.0.x', parse('10.0.3')), - })).toBe(true); - }); - - it('should not activate if release-candidate release-train is active', async () => { - expect(await CutReleaseCandidateForFeatureFreezeAction.isActive({ - // No longer in feature-freeze but in release-candidate phase. - releaseCandidate: new ReleaseTrain('10.1.x', parse('10.1.0-rc.0')), - next: new ReleaseTrain('master', parse('10.2.0-next.0')), - latest: new ReleaseTrain('10.0.x', parse('10.0.3')), - })).toBe(false); - }); - - it('should not activate if no FF/RC release-train is active', async () => { - expect(await CutReleaseCandidateForFeatureFreezeAction.isActive({ - releaseCandidate: null, - next: new ReleaseTrain('master', parse('10.1.0-next.0')), - latest: new ReleaseTrain('10.0.x', parse('10.0.3')), - })).toBe(false); - }); - - it('should create a proper new version and select correct branch', async () => { - const action = setupReleaseActionForTesting(CutReleaseCandidateForFeatureFreezeAction, { - releaseCandidate: new ReleaseTrain('10.1.x', parse('10.1.0-next.1')), - next: new ReleaseTrain('master', parse('10.2.0-next.0')), - latest: new ReleaseTrain('10.0.x', parse('10.0.3')), - }); - - await expectStagingAndPublishWithCherryPick(action, '10.1.x', '10.1.0-rc.0', 'next'); - }); -}); diff --git a/dev-infra/release/publish/test/cut-stable.spec.ts b/dev-infra/release/publish/test/cut-stable.spec.ts deleted file mode 100644 index 850a57b5a3fbb4..00000000000000 --- a/dev-infra/release/publish/test/cut-stable.spec.ts +++ /dev/null @@ -1,87 +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 {matchesVersion} from '../../../utils/testing/semver-matchers'; -import {ReleaseTrain} from '../../versioning/release-trains'; -import {CutStableAction} from '../actions/cut-stable'; -import * as externalCommands from '../external-commands'; - -import {expectStagingAndPublishWithCherryPick, parse, setupReleaseActionForTesting} from './test-utils'; - -describe('cut stable action', () => { - it('should not activate if a feature-freeze release-train is active', async () => { - expect(await CutStableAction.isActive({ - releaseCandidate: new ReleaseTrain('10.1.x', parse('10.1.0-next.1')), - next: new ReleaseTrain('master', parse('10.2.0-next.0')), - latest: new ReleaseTrain('10.0.x', parse('10.0.3')), - })).toBe(false); - }); - - it('should activate if release-candidate release-train is active', async () => { - expect(await CutStableAction.isActive({ - // No longer in feature-freeze but in release-candidate phase. - releaseCandidate: new ReleaseTrain('10.1.x', parse('10.1.0-rc.0')), - next: new ReleaseTrain('master', parse('10.2.0-next.0')), - latest: new ReleaseTrain('10.0.x', parse('10.0.3')), - })).toBe(true); - }); - - it('should not activate if no FF/RC release-train is active', async () => { - expect(await CutStableAction.isActive({ - releaseCandidate: null, - next: new ReleaseTrain('master', parse('10.1.0-next.0')), - latest: new ReleaseTrain('10.0.x', parse('10.0.3')), - })).toBe(false); - }); - - it('should create a proper new version and select correct branch', async () => { - const action = setupReleaseActionForTesting(CutStableAction, { - // No longer in feature-freeze but in release-candidate phase. - releaseCandidate: new ReleaseTrain('10.1.x', parse('10.1.0-rc.0')), - next: new ReleaseTrain('master', parse('10.2.0-next.0')), - latest: new ReleaseTrain('10.0.x', parse('10.0.3')), - }); - - await expectStagingAndPublishWithCherryPick(action, '10.1.x', '10.1.0', 'latest'); - }); - - it('should not tag the previous latest release-train if a minor has been cut', async () => { - const action = setupReleaseActionForTesting(CutStableAction, { - // No longer in feature-freeze but in release-candidate phase. - releaseCandidate: new ReleaseTrain('10.1.x', parse('10.1.0-rc.0')), - next: new ReleaseTrain('master', parse('10.2.0-next.0')), - latest: new ReleaseTrain('10.0.x', parse('10.0.3')), - }); - - await expectStagingAndPublishWithCherryPick(action, '10.1.x', '10.1.0', 'latest'); - expect(externalCommands.invokeSetNpmDistCommand).toHaveBeenCalledTimes(0); - }); - - it('should tag the previous latest release-train if a major has been cut', async () => { - const action = setupReleaseActionForTesting(CutStableAction, { - // No longer in feature-freeze but in release-candidate phase. - releaseCandidate: new ReleaseTrain('11.0.x', parse('11.0.0-rc.0')), - next: new ReleaseTrain('master', parse('10.2.0-next.0')), - latest: new ReleaseTrain('10.0.x', parse('10.0.3')), - }); - - // Ensure that the NPM dist tag is set only for packages that were available in the previous - // major version. A spy has already been installed on the function. - (externalCommands.invokeSetNpmDistCommand as jasmine.Spy).and.callFake(() => { - expect(action.gitClient.head.ref?.name).toBe('10.0.x'); - return Promise.resolve(); - }); - - // Major is released to the `next` NPM dist tag initially. Can be re-tagged with - // a separate release action. See `CutStableAction` for more details. - await expectStagingAndPublishWithCherryPick(action, '11.0.x', '11.0.0', 'next'); - expect(externalCommands.invokeSetNpmDistCommand).toHaveBeenCalledTimes(1); - expect(externalCommands.invokeSetNpmDistCommand) - .toHaveBeenCalledWith('v10-lts', matchesVersion('10.0.3')); - }); -}); diff --git a/dev-infra/release/publish/test/github-api-testing.ts b/dev-infra/release/publish/test/github-api-testing.ts deleted file mode 100644 index 084b9097dc8921..00000000000000 --- a/dev-infra/release/publish/test/github-api-testing.ts +++ /dev/null @@ -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 * as nock from 'nock'; - -/** - * Class that represents a Github repository in testing. The class can be - * used to intercept and except Github API requests for release actions. - */ -export class GithubTestingRepo { - /** Github API endpoint. */ - private apiEndpoint = `https://api.github.com`; - - /** Github API url for the given repository. */ - private repoApiUrl = `${this.apiEndpoint}/repos/${this.owner}/${this.name}`; - - constructor(public owner: string, public name: string) {} - - expectPullRequestToBeCreated( - baseBranch: string, fork: GithubTestingRepo, forkBranch: string, prNumber: number): this { - const expectedHead = `${fork.owner}:${forkBranch}`; - nock(this.repoApiUrl) - .post('/pulls', ({base, head}) => base === baseBranch && head === expectedHead) - .reply(200, {number: prNumber}); - return this; - } - - expectBranchRequest(branchName: string, sha: string|null): this { - nock(this.repoApiUrl) - .get(`/branches/${branchName}`) - .reply(sha ? 200 : 404, sha ? {commit: {sha}} : undefined); - return this; - } - - expectFindForkRequest(fork: GithubTestingRepo): this { - nock(this.apiEndpoint) - .post( - '/graphql', - ({variables}) => variables.owner === this.owner && variables.name === this.name) - .reply(200, { - data: {repository: {forks: {nodes: [{owner: {login: fork.owner}, name: fork.name}]}}} - }); - return this; - } - - expectCommitStatusCheck(sha: string, state: 'success'|'pending'|'failure'): this { - nock(this.repoApiUrl).get(`/commits/${sha}/status`).reply(200, {state}).activeMocks(); - return this; - } - - expectPullRequestWait(prNumber: number): this { - // The pull request state could be queried multiple times, so we persist - // this mock request. By default, nock only mocks requests once. - nock(this.repoApiUrl).get(`/pulls/${prNumber}`).reply(200, {merged: true}).persist(); - return this; - } - - expectCommitRequest(sha: string, message: string): this { - nock(this.repoApiUrl).get(`/commits/${sha}`).reply(200, {commit: {message}}); - return this; - } - - expectTagToBeCreated(tagName: string, sha: string): this { - nock(this.repoApiUrl) - .post(`/git/refs`, b => b.ref === `refs/tags/${tagName}` && b.sha === sha) - .reply(200, {}); - return this; - } - - expectReleaseToBeCreated(name: string, tagName: string): this { - nock(this.repoApiUrl) - .post('/releases', b => b.name === name && b['tag_name'] === tagName) - .reply(200, {}); - return this; - } -} diff --git a/dev-infra/release/publish/test/move-next-into-feature-freeze.spec.ts b/dev-infra/release/publish/test/move-next-into-feature-freeze.spec.ts deleted file mode 100644 index bf84b90b6bfb9b..00000000000000 --- a/dev-infra/release/publish/test/move-next-into-feature-freeze.spec.ts +++ /dev/null @@ -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 {ReleaseTrain} from '../../versioning/release-trains'; -import {MoveNextIntoFeatureFreezeAction} from '../actions/move-next-into-feature-freeze'; - -import {expectBranchOffActionToRun} from './branch-off-next-branch-testing'; -import {parse} from './test-utils'; - -describe('move next into feature-freeze action', () => { - it('should not activate if a feature-freeze release-train is active', async () => { - expect(await MoveNextIntoFeatureFreezeAction.isActive({ - releaseCandidate: new ReleaseTrain('10.1.x', parse('10.1.0-next.1')), - next: new ReleaseTrain('master', parse('10.2.0-next.0')), - latest: new ReleaseTrain('10.0.x', parse('10.0.3')), - })).toBe(false); - }); - - it('should not activate if release-candidate release-train is active', async () => { - expect(await MoveNextIntoFeatureFreezeAction.isActive({ - // No longer in feature-freeze but in release-candidate phase. - releaseCandidate: new ReleaseTrain('10.1.x', parse('10.1.0-rc.0')), - next: new ReleaseTrain('master', parse('10.2.0-next.0')), - latest: new ReleaseTrain('10.0.x', parse('10.0.3')), - })).toBe(false); - }); - - it('should not activate if the next release-train is for a minor', async () => { - expect(await MoveNextIntoFeatureFreezeAction.isActive({ - releaseCandidate: null, - next: new ReleaseTrain('master', parse('10.1.0-next.2')), - latest: new ReleaseTrain('10.0.x', parse('10.0.3')), - })).toBe(false); - }); - - it('should activate if no FF/RC release-train is active', async () => { - expect(await MoveNextIntoFeatureFreezeAction.isActive({ - releaseCandidate: null, - next: new ReleaseTrain('master', parse('11.0.0-next.0')), - latest: new ReleaseTrain('10.0.x', parse('10.0.3')), - })).toBe(true); - }); - - it('should create pull requests and feature-freeze branch', async () => { - await expectBranchOffActionToRun( - MoveNextIntoFeatureFreezeAction, { - releaseCandidate: null, - next: new ReleaseTrain('master', parse('10.2.0-next.0')), - latest: new ReleaseTrain('10.0.x', parse('10.0.3')), - }, - /* isNextPublishedToNpm */ true, '10.3.0-next.0', '10.2.0-next.1', '10.2.x'); - }); - - it('should not increment the version if "next" version is not yet published', async () => { - await expectBranchOffActionToRun( - MoveNextIntoFeatureFreezeAction, { - releaseCandidate: null, - next: new ReleaseTrain('master', parse('10.2.0-next.0')), - latest: new ReleaseTrain('10.0.x', parse('10.0.3')), - }, - /* isNextPublishedToNpm */ false, '10.3.0-next.0', '10.2.0-next.0', '10.2.x'); - }); -}); diff --git a/dev-infra/release/publish/test/move-next-into-release-candidate.spec.ts b/dev-infra/release/publish/test/move-next-into-release-candidate.spec.ts deleted file mode 100644 index d12b2ae159fe43..00000000000000 --- a/dev-infra/release/publish/test/move-next-into-release-candidate.spec.ts +++ /dev/null @@ -1,58 +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 {ReleaseTrain} from '../../versioning/release-trains'; -import {MoveNextIntoReleaseCandidateAction} from '../actions/move-next-into-release-candidate'; - -import {expectBranchOffActionToRun} from './branch-off-next-branch-testing'; -import {parse} from './test-utils'; - -describe('move next into release-candidate action', () => { - it('should not activate if a feature-freeze release-train is active', async () => { - expect(await MoveNextIntoReleaseCandidateAction.isActive({ - releaseCandidate: new ReleaseTrain('10.1.x', parse('10.1.0-next.1')), - next: new ReleaseTrain('master', parse('10.2.0-next.0')), - latest: new ReleaseTrain('10.0.x', parse('10.0.3')), - })).toBe(false); - }); - - it('should not activate if release-candidate release-train is active', async () => { - expect(await MoveNextIntoReleaseCandidateAction.isActive({ - // No longer in feature-freeze but in release-candidate phase. - releaseCandidate: new ReleaseTrain('10.1.x', parse('10.1.0-rc.0')), - next: new ReleaseTrain('master', parse('10.2.0-next.0')), - latest: new ReleaseTrain('10.0.x', parse('10.0.3')), - })).toBe(false); - }); - - it('should not activate if the next release-train is for a major', async () => { - expect(await MoveNextIntoReleaseCandidateAction.isActive({ - releaseCandidate: null, - next: new ReleaseTrain('master', parse('11.0.0-next.0')), - latest: new ReleaseTrain('10.0.x', parse('10.0.3')), - })).toBe(false); - }); - - it('should activate if no FF/RC release-train is active', async () => { - expect(await MoveNextIntoReleaseCandidateAction.isActive({ - releaseCandidate: null, - next: new ReleaseTrain('master', parse('10.1.0-next.0')), - latest: new ReleaseTrain('10.0.x', parse('10.0.3')), - })).toBe(true); - }); - - it('should create pull requests and new version-branch', async () => { - await expectBranchOffActionToRun( - MoveNextIntoReleaseCandidateAction, { - releaseCandidate: null, - next: new ReleaseTrain('master', parse('10.2.0-next.0')), - latest: new ReleaseTrain('10.0.x', parse('10.0.3')), - }, - /* isNextPublishedToNpm */ true, '10.3.0-next.0', '10.2.0-rc.0', '10.2.x'); - }); -}); diff --git a/dev-infra/release/publish/test/release-notes/context.spec.ts b/dev-infra/release/publish/test/release-notes/context.spec.ts deleted file mode 100644 index 40553742cc3fa2..00000000000000 --- a/dev-infra/release/publish/test/release-notes/context.spec.ts +++ /dev/null @@ -1,168 +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 {CommitFromGitLog, parseCommitFromGitLog} from '../../../../commit-message/parse'; -import {commitMessageBuilder} from '../../../../commit-message/test-util'; -import {RenderContext, RenderContextData,} from '../../../notes/context'; - -const defaultContextData: RenderContextData = { - commits: [], - github: { - name: 'repoName', - owner: 'repoOwner', - }, - title: false, - version: '1.2.3', -}; - -describe('RenderContext', () => { - beforeAll(() => { - jasmine.clock().install(); - }); - - it('contains a date stamp using the current date by default', async () => { - jasmine.clock().mockDate(new Date(1996, 11, 11)); - const renderContext = new RenderContext(defaultContextData); - expect(renderContext.dateStamp).toBe('1996-12-11'); - }); - - it('contains a date stamp using a provided date', async () => { - const data = {...defaultContextData, date: new Date(2000, 0, 20)}; - const renderContext = new RenderContext(data); - expect(renderContext.dateStamp).toBe('2000-01-20'); - }); - - it('filters to include only commits which have specified field', () => { - const renderContext = new RenderContext(defaultContextData); - const matchingCommits = commitsFromList(2, 15); - expect(commits.filter(renderContext.contains('breakingChanges'))).toEqual(matchingCommits); - }); - - it('filters to include only the first commit discovered with a unique value for a specified field', - () => { - const renderContext = new RenderContext(defaultContextData); - const matchingCommits = commitsFromList(0, 1, 2, 3, 4, 7, 12); - expect(commits.filter(renderContext.unique('type'))).toEqual(matchingCommits); - }); - - - describe('filters to include commits which are to be included in the release notes', () => { - it('including all scopes by default', () => { - const renderContext = new RenderContext(defaultContextData); - const matchingCommits = commitsFromList(0, 2, 5, 6, 8, 10, 11, 12, 15, 16); - expect(commits.filter(renderContext.includeInReleaseNotes())).toEqual(matchingCommits); - }); - - it('excluding hidden scopes defined in the config', () => { - const renderContext = new RenderContext({...defaultContextData, hiddenScopes: ['core']}); - const matchingCommits = commitsFromList(0, 2, 6, 8, 10, 11, 15, 16); - expect(commits.filter(renderContext.includeInReleaseNotes())).toEqual(matchingCommits); - }); - }); - - describe('organized lists of commits into groups', () => { - let devInfraCommits: CommitFromGitLog[]; - let coreCommits: CommitFromGitLog[]; - let compilerCommits: CommitFromGitLog[]; - let unorganizedCommits: CommitFromGitLog[]; - function assertOrganizedGroupsMatch( - generatedGroups: {title: string, commits: CommitFromGitLog[]}[], - providedGroups: {title: string, commits: CommitFromGitLog[]}[]) { - expect(generatedGroups.length).toBe(providedGroups.length); - generatedGroups.forEach(({title, commits}, idx) => { - expect(title).toBe(providedGroups[idx].title); - expect(commits).toEqual(jasmine.arrayWithExactContents(providedGroups[idx].commits)); - }); - } - - beforeEach(() => { - devInfraCommits = commits.filter(c => c.scope === 'dev-infra'); - coreCommits = commits.filter(c => c.scope === 'core'); - compilerCommits = commits.filter(c => c.scope === 'compiler'); - unorganizedCommits = - [...devInfraCommits, ...coreCommits, ...compilerCommits].sort(() => Math.random() - 0.5); - }); - - - - it('with default sorting', () => { - const renderContext = new RenderContext(defaultContextData); - const organizedCommits = renderContext.asCommitGroups(unorganizedCommits); - - assertOrganizedGroupsMatch(organizedCommits, [ - {title: 'compiler', commits: compilerCommits}, - {title: 'core', commits: coreCommits}, - {title: 'dev-infra', commits: devInfraCommits}, - ]); - }); - - it('sorted by the provided order in the config', () => { - const renderContext = - new RenderContext({...defaultContextData, groupOrder: ['core', 'dev-infra']}); - const organizedCommits = renderContext.asCommitGroups(unorganizedCommits); - - assertOrganizedGroupsMatch(organizedCommits, [ - {title: 'core', commits: coreCommits}, - {title: 'dev-infra', commits: devInfraCommits}, - {title: 'compiler', commits: compilerCommits}, - ]); - }); - }); - - afterAll(() => { - jasmine.clock().uninstall(); - }); -}); - - - -const buildCommitMessage = commitMessageBuilder({ - prefix: '', - type: '', - npmScope: '', - scope: '', - summary: 'This is a short summary of the change', - body: 'This is a longer description of the change', - footer: '', -}); - -function buildCommit(type: string, scope: string, withBreakingChange = false) { - const footer = withBreakingChange ? 'BREAKING CHANGE: something is broken now' : ''; - const parts = {type, scope, footer}; - return parseCommitFromGitLog(Buffer.from(buildCommitMessage(parts))); -} - - -function commitsFromList(...indexes: number[]) { - const output: CommitFromGitLog[] = []; - for (const i of indexes) { - output.push(commits[i]); - } - return output; -} - - -const commits: CommitFromGitLog[] = [ - buildCommit('fix', 'platform-browser'), - buildCommit('test', 'dev-infra'), - buildCommit('feat', 'dev-infra', true), - buildCommit('build', 'docs-infra'), - buildCommit('docs', 'router'), - buildCommit('feat', 'core'), - buildCommit('feat', 'common'), - buildCommit('refactor', 'compiler'), - buildCommit('fix', 'docs-infra'), - buildCommit('test', 'core'), - buildCommit('feat', 'compiler-cli'), - buildCommit('fix', 'dev-infra'), - buildCommit('perf', 'core'), - buildCommit('docs', 'forms'), - buildCommit('refactor', 'dev-infra'), - buildCommit('feat', 'docs-infra', true), - buildCommit('fix', 'compiler'), -]; diff --git a/dev-infra/release/publish/test/release-notes/release-notes-utils.ts b/dev-infra/release/publish/test/release-notes/release-notes-utils.ts deleted file mode 100644 index 1f7a4ad32d8858..00000000000000 --- a/dev-infra/release/publish/test/release-notes/release-notes-utils.ts +++ /dev/null @@ -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 - */ - -import * as semver from 'semver'; - -import {DevInfraReleaseConfig, ReleaseConfig} from '../../../config'; -import {ReleaseNotes} from '../../../notes/release-notes'; - -/** - * Mock version of the ReleaseNotes for testing, preventing actual calls to git for commits and - * returning versioned entry strings. - */ -class MockReleaseNotes extends ReleaseNotes { - static override async fromRange(version: semver.SemVer, startingRef: string, endingRef: string) { - return new MockReleaseNotes(version, startingRef, endingRef); - } - - override async getChangelogEntry() { - return `Changelog Entry for ${this.version}`; - } - - override async getGithubReleaseEntry() { - return `Github Release Entry for ${this.version}`; - } - - // Overrides of utility functions which call out to other tools and are unused in this mock. - protected override async getCommitsInRange(from: string, to?: string) { - return []; - } - protected override getReleaseConfig(config?: Partial) { - return {} as ReleaseConfig; - } -} - -/** Replace the ReleaseNotes static builder function with the MockReleaseNotes builder function. */ -export function installMockReleaseNotes() { - spyOn(ReleaseNotes, 'fromRange').and.callFake(MockReleaseNotes.fromRange); -} diff --git a/dev-infra/release/publish/test/tag-recent-major-as-latest.spec.ts b/dev-infra/release/publish/test/tag-recent-major-as-latest.spec.ts deleted file mode 100644 index 0bd42463c2cfcd..00000000000000 --- a/dev-infra/release/publish/test/tag-recent-major-as-latest.spec.ts +++ /dev/null @@ -1,116 +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 {matchesVersion} from '../../../utils/testing'; -import {ReleaseTrain} from '../../versioning/release-trains'; -import {TagRecentMajorAsLatest} from '../actions/tag-recent-major-as-latest'; -import * as externalCommands from '../external-commands'; - -import {fakeNpmPackageQueryRequest, getTestingMocksForReleaseAction, parse, setupReleaseActionForTesting} from './test-utils'; - -describe('tag recent major as latest action', () => { - it('should not be active if a patch has been published after major release', async () => { - const {releaseConfig} = getTestingMocksForReleaseAction(); - expect(await TagRecentMajorAsLatest.isActive( - { - releaseCandidate: null, - next: new ReleaseTrain('master', parse('10.1.0-next.0')), - latest: new ReleaseTrain('10.0.x', parse('10.0.1')), - }, - releaseConfig)) - .toBe(false); - }); - - it('should not be active if a major has been released recently but "@latest" on NPM points to ' + - 'a more recent major', - async () => { - const {releaseConfig} = getTestingMocksForReleaseAction(); - - // NPM `@latest` will point to a patch release of a more recent major. This is unlikely - // to happen (only with manual changes outside of the release tool), but should - // prevent accidental overrides from the release action. - fakeNpmPackageQueryRequest( - releaseConfig.npmPackages[0], {'dist-tags': {'latest': '11.0.3'}}); - - expect(await TagRecentMajorAsLatest.isActive( - { - releaseCandidate: null, - next: new ReleaseTrain('master', parse('10.1.0-next.0')), - latest: new ReleaseTrain('10.0.x', parse('10.0.0')), - }, - releaseConfig)) - .toBe(false); - }); - - it('should not be active if a major has been released recently but "@latest" on NPM points to ' + - 'an older major', - async () => { - const {releaseConfig} = getTestingMocksForReleaseAction(); - - // NPM `@latest` will point to a patch release of an older major. This is unlikely to happen - // (only with manual changes outside of the release tool), but should prevent accidental - // changes from the release action that indicate mismatched version branches, or an - // out-of-sync NPM registry. - fakeNpmPackageQueryRequest(releaseConfig.npmPackages[0], {'dist-tags': {'latest': '8.4.7'}}); - - expect(await TagRecentMajorAsLatest.isActive( - { - releaseCandidate: null, - next: new ReleaseTrain('master', parse('10.1.0-next.0')), - latest: new ReleaseTrain('10.0.x', parse('10.0.0')), - }, - releaseConfig)) - .toBe(false); - }); - - - it('should be active if a major has been released recently but is not published as ' + - '"@latest" to NPM', - async () => { - const {releaseConfig} = getTestingMocksForReleaseAction(); - - // NPM `@latest` will point to a patch release of the previous major. - fakeNpmPackageQueryRequest(releaseConfig.npmPackages[0], {'dist-tags': {'latest': '9.2.3'}}); - - expect(await TagRecentMajorAsLatest.isActive( - { - releaseCandidate: null, - next: new ReleaseTrain('master', parse('10.1.0-next.0')), - latest: new ReleaseTrain('10.0.x', parse('10.0.0')), - }, - releaseConfig)) - .toBe(true); - }); - - it('should be active if a major has been released recently but is not published as ' + - '"@latest" to NPM', - async () => { - const {instance, gitClient, releaseConfig} = - setupReleaseActionForTesting(TagRecentMajorAsLatest, { - releaseCandidate: null, - next: new ReleaseTrain('master', parse('10.1.0-next.0')), - latest: new ReleaseTrain('10.0.x', parse('10.0.0')), - }); - - // NPM `@latest` will point to a patch release of the previous major. - fakeNpmPackageQueryRequest(releaseConfig.npmPackages[0], {'dist-tags': {'latest': '9.2.3'}}); - - await instance.perform(); - - // Ensure that the NPM dist tag is set only for packages that were available in the previous - // major version. A spy has already been installed on the function. - (externalCommands.invokeSetNpmDistCommand as jasmine.Spy).and.callFake(() => { - expect(gitClient.head.ref?.name).toBe('10.0.x'); - return Promise.resolve(); - }); - - expect(externalCommands.invokeSetNpmDistCommand).toHaveBeenCalledTimes(1); - expect(externalCommands.invokeSetNpmDistCommand) - .toHaveBeenCalledWith('latest', matchesVersion('10.0.0')); - }); -}); diff --git a/dev-infra/release/publish/test/test-utils.ts b/dev-infra/release/publish/test/test-utils.ts deleted file mode 100644 index 3681e3c2c0290c..00000000000000 --- a/dev-infra/release/publish/test/test-utils.ts +++ /dev/null @@ -1,245 +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 * as nock from 'nock'; -import {join} from 'path'; -import * as semver from 'semver'; - -import {GithubConfig} from '../../../utils/config'; -import * as console from '../../../utils/console'; -import {getBranchPushMatcher, installVirtualGitClientSpies, VirtualGitClient} from '../../../utils/testing'; -import {ReleaseConfig} from '../../config/index'; -import {ActiveReleaseTrains} from '../../versioning/active-release-trains'; -import * as npm from '../../versioning/npm-publish'; -import {_npmPackageInfoCache, NpmDistTag, NpmPackageInfo} from '../../versioning/npm-registry'; -import {ReleaseAction, ReleaseActionConstructor} from '../actions'; -import * as constants from '../constants'; -import * as externalCommands from '../external-commands'; - -import {GithubTestingRepo} from './github-api-testing'; -import {installMockReleaseNotes} from './release-notes/release-notes-utils'; - -/** - * Temporary directory which will be used as project directory in tests. Note that - * this environment variable is automatically set by Bazel for tests. - */ -export const testTmpDir: string = process.env['TEST_TMPDIR']!; - -/** Interface describing a test release action. */ -export interface TestReleaseAction { - instance: T; - gitClient: VirtualGitClient; - repo: GithubTestingRepo; - fork: GithubTestingRepo; - testTmpDir: string; - githubConfig: GithubConfig; - releaseConfig: ReleaseConfig; -} - -/** Gets necessary test mocks for running a release action. */ -export function getTestingMocksForReleaseAction() { - const githubConfig = {owner: 'angular', name: 'dev-infra-test'}; - const gitClient = VirtualGitClient.createInstance({github: githubConfig}); - const releaseConfig: ReleaseConfig = { - npmPackages: [ - '@angular/pkg1', - '@angular/pkg2', - ], - releaseNotes: {}, - buildPackages: () => { - throw Error('Not implemented'); - }, - }; - return {githubConfig, gitClient, releaseConfig}; -} - -/** - * Sets up the given release action for testing. - * @param actionCtor Type of release action to be tested. - * @param active Fake active release trains for the action, - * @param isNextPublishedToNpm Whether the next version is published to NPM. True by default. - */ -export function setupReleaseActionForTesting( - actionCtor: ReleaseActionConstructor, active: ActiveReleaseTrains, - isNextPublishedToNpm = true): TestReleaseAction { - // Reset existing HTTP interceptors. - nock.cleanAll(); - - const {gitClient, githubConfig, releaseConfig} = getTestingMocksForReleaseAction(); - const repo = new GithubTestingRepo(githubConfig.owner, githubConfig.name); - const fork = new GithubTestingRepo('some-user', 'fork'); - - installVirtualGitClientSpies(gitClient); - installMockReleaseNotes(); - - // The version for the release-train in the next phase does not necessarily need to be - // published to NPM. We mock the NPM package request and fake the state of the next - // version based on the `isNextPublishedToNpm` testing parameter. More details on the - // special case for the next release train can be found in the next pre-release action. - fakeNpmPackageQueryRequest( - releaseConfig.npmPackages[0], - {versions: {[active.next.version.format()]: isNextPublishedToNpm ? {} : undefined}}); - - const action = new actionCtor(active, gitClient, releaseConfig, testTmpDir); - - // Fake confirm any prompts. We do not want to make any changelog edits and - // just proceed with the release action. - spyOn(console, 'promptConfirm').and.resolveTo(true); - - // Fake all external commands for the release tool. - spyOn(npm, 'runNpmPublish').and.resolveTo(); - spyOn(externalCommands, 'invokeSetNpmDistCommand').and.resolveTo(); - spyOn(externalCommands, 'invokeYarnInstallCommand').and.resolveTo(); - spyOn(externalCommands, 'invokeReleaseBuildCommand').and.resolveTo([ - {name: '@angular/pkg1', outputPath: `${testTmpDir}/dist/pkg1`}, - {name: '@angular/pkg2', outputPath: `${testTmpDir}/dist/pkg2`} - ]); - - // Fake checking the package versions since we don't actually create packages to check against in - // the publish tests. - spyOn(ReleaseAction.prototype, '_verifyPackageVersions' as any).and.resolveTo(); - - // Create an empty changelog and a `package.json` file so that file system - // interactions with the project directory do not cause exceptions. - writeFileSync(join(testTmpDir, 'CHANGELOG.md'), 'Existing changelog'); - writeFileSync(join(testTmpDir, 'package.json'), JSON.stringify({version: '0.0.0'})); - - // Override the default pull request wait interval to a number of milliseconds that can be - // awaited in Jasmine tests. The default interval of 10sec is too large and causes a timeout. - Object.defineProperty(constants, 'waitForPullRequestInterval', {value: 50}); - - return {instance: action, repo, fork, testTmpDir, githubConfig, releaseConfig, gitClient}; -} - -/** Parses the specified version into Semver. */ -export function parse(version: string): semver.SemVer { - return semver.parse(version)!; -} - -export async function expectStagingAndPublishWithoutCherryPick( - action: TestReleaseAction, expectedBranch: string, expectedVersion: string, - expectedNpmDistTag: NpmDistTag) { - const {repo, fork, gitClient} = action; - const expectedStagingForkBranch = `release-stage-${expectedVersion}`; - const expectedTagName = expectedVersion; - - // We first mock the commit status check for the next branch, then expect two pull - // requests from a fork that are targeting next and the new feature-freeze branch. - repo.expectBranchRequest(expectedBranch, 'MASTER_COMMIT_SHA') - .expectCommitStatusCheck('MASTER_COMMIT_SHA', 'success') - .expectFindForkRequest(fork) - .expectPullRequestToBeCreated(expectedBranch, fork, expectedStagingForkBranch, 200) - .expectPullRequestWait(200) - .expectBranchRequest(expectedBranch, 'STAGING_COMMIT_SHA') - .expectCommitRequest( - 'STAGING_COMMIT_SHA', `release: cut the v${expectedVersion} release\n\nPR Close #200.`) - .expectTagToBeCreated(expectedTagName, 'STAGING_COMMIT_SHA') - .expectReleaseToBeCreated(`v${expectedVersion}`, expectedTagName); - - // In the fork, we make the staging branch appear as non-existent, - // so that the PR can be created properly without collisions. - fork.expectBranchRequest(expectedStagingForkBranch, null); - - await action.instance.perform(); - - expect(gitClient.pushed.length).toBe(1); - expect(gitClient.pushed[0]) - .toEqual( - getBranchPushMatcher({ - baseBranch: expectedBranch, - baseRepo: repo, - targetBranch: expectedStagingForkBranch, - targetRepo: fork, - expectedCommits: [{ - message: `release: cut the v${expectedVersion} release`, - files: ['package.json', 'CHANGELOG.md'], - }], - }), - 'Expected release staging branch to be created in fork.'); - - expect(externalCommands.invokeReleaseBuildCommand).toHaveBeenCalledTimes(1); - expect(npm.runNpmPublish).toHaveBeenCalledTimes(2); - expect(npm.runNpmPublish) - .toHaveBeenCalledWith(`${testTmpDir}/dist/pkg1`, expectedNpmDistTag, undefined); - expect(npm.runNpmPublish) - .toHaveBeenCalledWith(`${testTmpDir}/dist/pkg2`, expectedNpmDistTag, undefined); -} - -export async function expectStagingAndPublishWithCherryPick( - action: TestReleaseAction, expectedBranch: string, expectedVersion: string, - expectedNpmDistTag: NpmDistTag) { - const {repo, fork, gitClient, releaseConfig} = action; - const expectedStagingForkBranch = `release-stage-${expectedVersion}`; - const expectedCherryPickForkBranch = `changelog-cherry-pick-${expectedVersion}`; - const expectedTagName = expectedVersion; - - // We first mock the commit status check for the next branch, then expect two pull - // requests from a fork that are targeting next and the new feature-freeze branch. - repo.expectBranchRequest(expectedBranch, 'MASTER_COMMIT_SHA') - .expectCommitStatusCheck('MASTER_COMMIT_SHA', 'success') - .expectFindForkRequest(fork) - .expectPullRequestToBeCreated(expectedBranch, fork, expectedStagingForkBranch, 200) - .expectPullRequestWait(200) - .expectBranchRequest(expectedBranch, 'STAGING_COMMIT_SHA') - .expectCommitRequest( - 'STAGING_COMMIT_SHA', `release: cut the v${expectedVersion} release\n\nPR Close #200.`) - .expectTagToBeCreated(expectedTagName, 'STAGING_COMMIT_SHA') - .expectReleaseToBeCreated(`v${expectedVersion}`, expectedTagName) - .expectPullRequestToBeCreated('master', fork, expectedCherryPickForkBranch, 300) - .expectPullRequestWait(300); - - // In the fork, we make the staging and cherry-pick branches appear as - // non-existent, so that the PRs can be created properly without collisions. - fork.expectBranchRequest(expectedStagingForkBranch, null) - .expectBranchRequest(expectedCherryPickForkBranch, null); - - await action.instance.perform(); - - expect(gitClient.pushed.length).toBe(2); - expect(gitClient.pushed[0]) - .toEqual( - getBranchPushMatcher({ - baseBranch: expectedBranch, - baseRepo: repo, - targetBranch: expectedStagingForkBranch, - targetRepo: fork, - expectedCommits: [{ - message: `release: cut the v${expectedVersion} release`, - files: ['package.json', 'CHANGELOG.md'], - }], - }), - 'Expected release staging branch to be created in fork.'); - - expect(gitClient.pushed[1]) - .toEqual( - getBranchPushMatcher({ - baseBranch: 'master', - baseRepo: repo, - targetBranch: expectedCherryPickForkBranch, - targetRepo: fork, - expectedCommits: [{ - message: `docs: release notes for the v${expectedVersion} release`, - files: ['CHANGELOG.md'], - }], - }), - 'Expected cherry-pick branch to be created in fork.'); - - expect(externalCommands.invokeReleaseBuildCommand).toHaveBeenCalledTimes(1); - expect(npm.runNpmPublish).toHaveBeenCalledTimes(2); - expect(npm.runNpmPublish) - .toHaveBeenCalledWith(`${testTmpDir}/dist/pkg1`, expectedNpmDistTag, undefined); - expect(npm.runNpmPublish) - .toHaveBeenCalledWith(`${testTmpDir}/dist/pkg2`, expectedNpmDistTag, undefined); -} - -/** Fakes a NPM package query API request for the given package. */ -export function fakeNpmPackageQueryRequest(pkgName: string, data: Partial) { - _npmPackageInfoCache[pkgName] = - Promise.resolve({'dist-tags': {}, versions: {}, time: {}, ...data}); -} diff --git a/dev-infra/release/set-dist-tag/BUILD.bazel b/dev-infra/release/set-dist-tag/BUILD.bazel deleted file mode 100644 index 3e2f49d3fcf273..00000000000000 --- a/dev-infra/release/set-dist-tag/BUILD.bazel +++ /dev/null @@ -1,42 +0,0 @@ -load("//dev-infra:defaults.bzl", "jasmine_node_test", "ts_library") - -ts_library( - name = "set-dist-tag", - srcs = glob( - [ - "**/*.ts", - ], - exclude = ["*.spec.ts"], - ), - visibility = ["//dev-infra:__subpackages__"], - deps = [ - "//dev-infra/release/config", - "//dev-infra/release/versioning", - "//dev-infra/utils", - "@npm//@types/node", - "@npm//@types/semver", - "@npm//@types/yargs", - "@npm//ora", - "@npm//semver", - ], -) - -ts_library( - name = "test_lib", - srcs = glob([ - "*.spec.ts", - ]), - deps = [ - ":set-dist-tag", - "//dev-infra/release/config", - "//dev-infra/release/versioning", - "//dev-infra/utils/testing", - "@npm//@types/jasmine", - "@npm//@types/node", - ], -) - -jasmine_node_test( - name = "test", - deps = [":test_lib"], -) diff --git a/dev-infra/release/set-dist-tag/cli.ts b/dev-infra/release/set-dist-tag/cli.ts deleted file mode 100644 index 06c5e7e2a3b229..00000000000000 --- a/dev-infra/release/set-dist-tag/cli.ts +++ /dev/null @@ -1,78 +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 ora from 'ora'; -import * as semver from 'semver'; -import {Arguments, Argv, CommandModule} from 'yargs'; - -import {bold, debug, error, green, info, red} from '../../utils/console'; -import {getReleaseConfig} from '../config/index'; -import {setNpmTagForPackage} from '../versioning/npm-publish'; - - -/** Command line options for setting an NPM dist tag. */ -export interface ReleaseSetDistTagOptions { - tagName: string; - targetVersion: string; -} - -function builder(args: Argv): Argv { - return args - .positional('tagName', { - type: 'string', - demandOption: true, - description: 'Name of the NPM dist tag.', - }) - .positional('targetVersion', { - type: 'string', - demandOption: true, - description: 'Version to which the dist tag should be set.' - }); -} - -/** Yargs command handler for building a release. */ -async function handler(args: Arguments) { - const {targetVersion: rawVersion, tagName} = args; - const {npmPackages, publishRegistry} = getReleaseConfig(); - const version = semver.parse(rawVersion); - - if (version === null) { - error(red(`Invalid version specified (${rawVersion}). Unable to set NPM dist tag.`)); - process.exit(1); - } - - const spinner = ora.call(undefined).start(); - debug(`Setting "${tagName}" NPM dist tag for release packages to v${version}.`); - - for (const pkgName of npmPackages) { - spinner.text = `Setting NPM dist tag for "${pkgName}"`; - spinner.render(); - - try { - await setNpmTagForPackage(pkgName, tagName, version!, publishRegistry); - debug(`Successfully set "${tagName}" NPM dist tag for "${pkgName}".`); - } catch (e) { - spinner.stop(); - error(e); - error(red(` ✘ An error occurred while setting the NPM dist tag for "${pkgName}".`)); - process.exit(1); - } - } - - spinner.stop(); - info(green(` ✓ Set NPM dist tag for all release packages.`)); - info(green(` ${bold(tagName)} will now point to ${bold(`v${version}`)}.`)); -} - -/** CLI command module for setting an NPM dist tag. */ -export const ReleaseSetDistTagCommand: CommandModule<{}, ReleaseSetDistTagOptions> = { - builder, - handler, - command: 'set-dist-tag ', - describe: 'Sets a given NPM dist tag for all release packages.', -}; diff --git a/dev-infra/release/set-dist-tag/set-dist-tag.spec.ts b/dev-infra/release/set-dist-tag/set-dist-tag.spec.ts deleted file mode 100644 index 13cbe1064a9c4e..00000000000000 --- a/dev-infra/release/set-dist-tag/set-dist-tag.spec.ts +++ /dev/null @@ -1,73 +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 {matchesVersion} from '../../utils/testing/semver-matchers'; -import * as releaseConfig from '../config/index'; -import * as npm from '../versioning/npm-publish'; - -import {ReleaseSetDistTagCommand} from './cli'; - -describe('ng-dev release set-dist-tag', () => { - let npmPackages: string[]; - let publishRegistry: string|undefined; - - beforeEach(() => { - npmPackages = ['@angular/pkg1', '@angular/pkg2']; - publishRegistry = undefined; - - spyOn(npm, 'setNpmTagForPackage'); - // We need to stub out the `process.exit` function during tests as the - // CLI handler calls those in case of failures. - spyOn(process, 'exit'); - }); - - /** Invokes the `set-dist-tag` command handler. */ - async function invokeCommand(tagName: string, targetVersion: string) { - spyOn(releaseConfig, 'getReleaseConfig').and.returnValue({ - npmPackages, - publishRegistry, - buildPackages: async () => [], - releaseNotes: {}, - }); - await ReleaseSetDistTagCommand.handler({tagName, targetVersion, $0: '', _: []}); - } - - it('should invoke "npm dist-tag" command for all configured packages', async () => { - await invokeCommand('latest', '10.0.0'); - expect(npm.setNpmTagForPackage).toHaveBeenCalledTimes(2); - expect(npm.setNpmTagForPackage) - .toHaveBeenCalledWith('@angular/pkg1', 'latest', matchesVersion('10.0.0'), undefined); - expect(npm.setNpmTagForPackage) - .toHaveBeenCalledWith('@angular/pkg2', 'latest', matchesVersion('10.0.0'), undefined); - }); - - it('should support a configured custom NPM registry', async () => { - publishRegistry = 'https://my-custom-registry.angular.io'; - await invokeCommand('latest', '10.0.0'); - - expect(npm.setNpmTagForPackage).toHaveBeenCalledTimes(2); - expect(npm.setNpmTagForPackage) - .toHaveBeenCalledWith( - '@angular/pkg1', 'latest', matchesVersion('10.0.0'), - 'https://my-custom-registry.angular.io'); - expect(npm.setNpmTagForPackage) - .toHaveBeenCalledWith( - '@angular/pkg2', 'latest', matchesVersion('10.0.0'), - 'https://my-custom-registry.angular.io'); - }); - - it('should error if an invalid version has been specified', async () => { - spyOn(console, 'error'); - await invokeCommand('latest', '10.0'); - - expect(console.error) - .toHaveBeenCalledWith('Invalid version specified (10.0). Unable to set NPM dist tag.'); - expect(process.exit).toHaveBeenCalledWith(1); - expect(process.exit).toHaveBeenCalledTimes(1); - }); -}); diff --git a/dev-infra/release/stamping/cli.ts b/dev-infra/release/stamping/cli.ts deleted file mode 100644 index f6343df956003b..00000000000000 --- a/dev-infra/release/stamping/cli.ts +++ /dev/null @@ -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 {Arguments, Argv, CommandModule} from 'yargs'; - -import {buildEnvStamp, EnvStampMode} from './env-stamp'; - - -export interface Options { - mode: EnvStampMode; -} - -function builder(args: Argv): Argv { - return args.option('mode', { - demandOption: true, - description: 'Whether the env-stamp should be built for a snapshot or release', - choices: ['snapshot' as const, 'release' as const] - }); -} - -async function handler({mode}: Arguments) { - buildEnvStamp(mode); -} - -/** CLI command module for building the environment stamp. */ -export const BuildEnvStampCommand: CommandModule<{}, Options> = { - builder, - handler, - command: 'build-env-stamp', - describe: 'Build the environment stamping information', -}; diff --git a/dev-infra/release/stamping/env-stamp.ts b/dev-infra/release/stamping/env-stamp.ts deleted file mode 100644 index 3c9f9bd547265f..00000000000000 --- a/dev-infra/release/stamping/env-stamp.ts +++ /dev/null @@ -1,112 +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 {SemVer} from 'semver'; -import {GitClient} from '../../utils/git/git-client'; -import {createExperimentalSemver} from '../../utils/semver'; - -export type EnvStampMode = 'snapshot'|'release'; - -/** - * Log the environment variables expected by bazel for stamping. - * - * See the section on stamping in docs / BAZEL.md - * - * This script must be a NodeJS script in order to be cross-platform. - * See https://github.com/bazelbuild/bazel/issues/5958 - * Note: git operations, especially git status, take a long time inside mounted docker volumes - * in Windows or OSX hosts (https://github.com/docker/for-win/issues/188). - */ -export function buildEnvStamp(mode: EnvStampMode) { - console.info(`BUILD_SCM_BRANCH ${getCurrentBranch()}`); - console.info(`BUILD_SCM_COMMIT_SHA ${getCurrentBranchOrRevision()}`); - console.info(`BUILD_SCM_HASH ${getCurrentBranchOrRevision()}`); - console.info(`BUILD_SCM_LOCAL_CHANGES ${hasLocalChanges()}`); - console.info(`BUILD_SCM_USER ${getCurrentGitUser()}`); - const {version, experimentalVersion} = getSCMVersions(mode); - console.info(`BUILD_SCM_VERSION ${version}`); - console.info(`BUILD_SCM_EXPERIMENTAL_VERSION ${experimentalVersion}`); - process.exit(); -} - -/** Whether the repo has local changes. */ -function hasLocalChanges() { - try { - const git = GitClient.get(); - return git.hasUncommittedChanges(); - } catch { - return true; - } -} - -/** - * Get the versions for generated packages. - * - * In snapshot mode, the version is based on the most recent semver tag. - * In release mode, the version is based on the base package.json version. - */ -function getSCMVersions(mode: EnvStampMode): {version: string, experimentalVersion: string} { - try { - const git = GitClient.get(); - if (mode === 'snapshot') { - const localChanges = hasLocalChanges() ? '.with-local-changes' : ''; - const {stdout: rawVersion} = - git.run(['describe', '--match', '*[0-9]*.[0-9]*.[0-9]*', '--abbrev=7', '--tags', 'HEAD']); - const {version} = new SemVer(rawVersion); - const {version: experimentalVersion} = createExperimentalSemver(version); - return { - version: `${version.replace(/-([0-9]+)-g/, '+$1.sha-')}${localChanges}`, - experimentalVersion: - `${experimentalVersion.replace(/-([0-9]+)-g/, '+$1.sha-')}${localChanges}`, - }; - } else { - const packageJsonPath = join(git.baseDir, 'package.json'); - const {version} = new SemVer(require(packageJsonPath).version); - const {version: experimentalVersion} = createExperimentalSemver(new SemVer(version)); - return {version, experimentalVersion}; - } - } catch { - return { - version: '', - experimentalVersion: '', - }; - } -} - -/** Get the current branch or revision of HEAD. */ -function getCurrentBranchOrRevision() { - try { - const git = GitClient.get(); - return git.getCurrentBranchOrRevision(); - } catch { - return ''; - } -} - -/** Get the currently checked out branch. */ -function getCurrentBranch() { - try { - const git = GitClient.get(); - return git.run(['symbolic-ref', '--short', 'HEAD']).stdout.trim(); - } catch { - return ''; - } -} - -/** Get the current git user based on the git config. */ -function getCurrentGitUser() { - try { - const git = GitClient.get(); - let userName = git.runGraceful(['config', 'user.name']).stdout.trim() || 'Unknown User'; - let userEmail = git.runGraceful(['config', 'user.email']).stdout.trim() || 'unknown_email'; - return `${userName} <${userEmail}>`; - } catch { - return ''; - } -} diff --git a/dev-infra/release/versioning/BUILD.bazel b/dev-infra/release/versioning/BUILD.bazel deleted file mode 100644 index 7a486a5d8f3159..00000000000000 --- a/dev-infra/release/versioning/BUILD.bazel +++ /dev/null @@ -1,17 +0,0 @@ -load("//dev-infra:defaults.bzl", "ts_library") - -ts_library( - name = "versioning", - srcs = glob([ - "**/*.ts", - ]), - visibility = ["//dev-infra:__subpackages__"], - deps = [ - "//dev-infra/release/config", - "//dev-infra/utils", - "@npm//@types/node-fetch", - "@npm//@types/semver", - "@npm//node-fetch", - "@npm//semver", - ], -) diff --git a/dev-infra/release/versioning/README.md b/dev-infra/release/versioning/README.md deleted file mode 100644 index b428492b7f02e2..00000000000000 --- a/dev-infra/release/versioning/README.md +++ /dev/null @@ -1,5 +0,0 @@ -## Versioning for the Angular organization - -The folder contains common tooling needed for implementing the versioning as proposed -by [this design document](https://docs.google.com/document/d/197kVillDwx-RZtSVOBtPb4BBIAw0E9RT3q3v6DZkykU). -Primary tooling is the determination of _active_ release trains. diff --git a/dev-infra/release/versioning/active-release-trains.ts b/dev-infra/release/versioning/active-release-trains.ts deleted file mode 100644 index d123db02e4c2fb..00000000000000 --- a/dev-infra/release/versioning/active-release-trains.ts +++ /dev/null @@ -1,139 +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 semver from 'semver'; - -import {ReleaseTrain} from './release-trains'; -import {getBranchesForMajorVersions, getVersionOfBranch, GithubRepoWithApi, VersionBranch} from './version-branches'; - -/** Interface describing determined active release trains for a project. */ -export interface ActiveReleaseTrains { - /** Release-train currently in the "release-candidate" or "feature-freeze" phase. */ - releaseCandidate: ReleaseTrain|null; - /** Release-train currently in the "latest" phase. */ - latest: ReleaseTrain; - /** Release-train in the `next` phase. */ - next: ReleaseTrain; -} - -/** Branch name for the `next` branch. */ -export const nextBranchName = 'master'; - -/** Fetches the active release trains for the configured project. */ -export async function fetchActiveReleaseTrains(repo: GithubRepoWithApi): - Promise { - const nextVersion = await getVersionOfBranch(repo, nextBranchName); - const next = new ReleaseTrain(nextBranchName, nextVersion); - const majorVersionsToConsider: number[] = []; - let expectedReleaseCandidateMajor: number; - - // If the `next` branch (i.e. `master` branch) is for an upcoming major version, we know - // that there is no patch branch or feature-freeze/release-candidate branch for this major - // digit. If the current `next` version is the first minor of a major version, we know that - // the feature-freeze/release-candidate branch can only be the actual major branch. The - // patch branch is based on that, either the actual major branch or the last minor from the - // preceding major version. In all other cases, the patch branch and feature-freeze or - // release-candidate branch are part of the same major version. Consider the following: - // - // CASE 1. next: 11.0.0-next.0: patch and feature-freeze/release-candidate can only be - // most recent `10.<>.x` branches. The FF/RC branch can only be the last-minor of v10. - // CASE 2. next: 11.1.0-next.0: patch can be either `11.0.x` or last-minor in v10 based - // on whether there is a feature-freeze/release-candidate branch (=> `11.0.x`). - // CASE 3. next: 10.6.0-next.0: patch can be either `10.5.x` or `10.4.x` based on whether - // there is a feature-freeze/release-candidate branch (=> `10.5.x`) - if (nextVersion.minor === 0) { - expectedReleaseCandidateMajor = nextVersion.major - 1; - majorVersionsToConsider.push(nextVersion.major - 1); - } else if (nextVersion.minor === 1) { - expectedReleaseCandidateMajor = nextVersion.major; - majorVersionsToConsider.push(nextVersion.major, nextVersion.major - 1); - } else { - expectedReleaseCandidateMajor = nextVersion.major; - majorVersionsToConsider.push(nextVersion.major); - } - - // Collect all version-branches that should be considered for the latest version-branch, - // or the feature-freeze/release-candidate. - const branches = await getBranchesForMajorVersions(repo, majorVersionsToConsider); - const {latest, releaseCandidate} = await findActiveReleaseTrainsFromVersionBranches( - repo, nextVersion, branches, expectedReleaseCandidateMajor); - - if (latest === null) { - throw Error( - `Unable to determine the latest release-train. The following branches ` + - `have been considered: [${branches.map(b => b.name).join(', ')}]`); - } - - return {releaseCandidate, latest, next}; -} - -/** Finds the currently active release trains from the specified version branches. */ -export async function findActiveReleaseTrainsFromVersionBranches( - repo: GithubRepoWithApi, nextVersion: semver.SemVer, branches: VersionBranch[], - expectedReleaseCandidateMajor: number): Promise<{ - latest: ReleaseTrain | null, - releaseCandidate: ReleaseTrain | null, -}> { - // Version representing the release-train currently in the next phase. Note that we ignore - // patch and pre-release segments in order to be able to compare the next release train to - // other release trains from version branches (which follow the `N.N.x` pattern). - const nextReleaseTrainVersion = semver.parse(`${nextVersion.major}.${nextVersion.minor}.0`)!; - - let latest: ReleaseTrain|null = null; - let releaseCandidate: ReleaseTrain|null = null; - - // Iterate through the captured branches and find the latest non-prerelease branch and a - // potential release candidate branch. From the collected branches we iterate descending - // order (most recent semantic version-branch first). The first branch is either the latest - // active version branch (i.e. patch) or a feature-freeze/release-candidate branch. A FF/RC - // branch cannot be older than the latest active version-branch, so we stop iterating once - // we found such a branch. Otherwise, if we found a FF/RC branch, we continue looking for the - // next version-branch as that one is supposed to be the latest active version-branch. If it - // is not, then an error will be thrown due to two FF/RC branches existing at the same time. - for (const {name, parsed} of branches) { - // It can happen that version branches have been accidentally created which are more recent - // than the release-train in the next branch (i.e. `master`). We could ignore such branches - // silently, but it might be symptomatic for an outdated version in the `next` branch, or an - // accidentally created branch by the caretaker. In either way we want to raise awareness. - if (semver.gt(parsed, nextReleaseTrainVersion)) { - throw Error( - `Discovered unexpected version-branch "${name}" for a release-train that is ` + - `more recent than the release-train currently in the "${nextBranchName}" branch. ` + - `Please either delete the branch if created by accident, or update the outdated ` + - `version in the next branch (${nextBranchName}).`); - } else if (semver.eq(parsed, nextReleaseTrainVersion)) { - throw Error( - `Discovered unexpected version-branch "${name}" for a release-train that is already ` + - `active in the "${nextBranchName}" branch. Please either delete the branch if ` + - `created by accident, or update the version in the next branch (${nextBranchName}).`); - } - - const version = await getVersionOfBranch(repo, name); - const releaseTrain = new ReleaseTrain(name, version); - const isPrerelease = version.prerelease[0] === 'rc' || version.prerelease[0] === 'next'; - - if (isPrerelease) { - if (releaseCandidate !== null) { - throw Error( - `Unable to determine latest release-train. Found two consecutive ` + - `branches in feature-freeze/release-candidate phase. Did not expect both "${name}" ` + - `and "${releaseCandidate.branchName}" to be in feature-freeze/release-candidate mode.`); - } else if (version.major !== expectedReleaseCandidateMajor) { - throw Error( - `Discovered unexpected old feature-freeze/release-candidate branch. Expected no ` + - `version-branch in feature-freeze/release-candidate mode for v${version.major}.`); - } - releaseCandidate = releaseTrain; - } else { - latest = releaseTrain; - break; - } - } - - return {releaseCandidate, latest}; -} diff --git a/dev-infra/release/versioning/index.ts b/dev-infra/release/versioning/index.ts deleted file mode 100644 index fa834458cd150b..00000000000000 --- a/dev-infra/release/versioning/index.ts +++ /dev/null @@ -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 - */ - -export * from './active-release-trains'; -export * from './release-trains'; -export * from './long-term-support'; -export * from './version-branches'; -export * from './npm-registry'; diff --git a/dev-infra/release/versioning/long-term-support.ts b/dev-infra/release/versioning/long-term-support.ts deleted file mode 100644 index 676b85b2fd4c72..00000000000000 --- a/dev-infra/release/versioning/long-term-support.ts +++ /dev/null @@ -1,107 +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 semver from 'semver'; - -import {ReleaseConfig} from '../config/index'; - -import {fetchProjectNpmPackageInfo} from './npm-registry'; - -/** Type describing a NPM dist tag indicating long-term support. */ -export type LtsNpmDistTag = `v${number}-lts`; - -/** Interface describing determined LTS branches. */ -export interface LtsBranches { - /** List of active LTS version branches. */ - active: LtsBranch[]; - /** List of inactive LTS version branches. */ - inactive: LtsBranch[]; -} - -/** Interface describing an LTS version branch. */ -export interface LtsBranch { - /** Name of the branch. */ - name: string; - /** Most recent version for the given LTS branch. */ - version: semver.SemVer; - /** NPM dist tag for the LTS version. */ - npmDistTag: LtsNpmDistTag; -} - -/** - * Number of months a major version in Angular is actively supported. See: - * https://angular.io/guide/releases#support-policy-and-schedule. - */ -const majorActiveSupportDuration = 6; - -/** - * Number of months a major version has active long-term support. See: - * https://angular.io/guide/releases#support-policy-and-schedule. - */ -const majorLongTermSupportDuration = 12; - -/** Regular expression that matches LTS NPM dist tags. */ -const ltsNpmDistTagRegex = /^v(\d+)-lts$/; - -/** Finds all long-term support release trains from the specified NPM package. */ -export async function fetchLongTermSupportBranchesFromNpm(config: ReleaseConfig): - Promise { - const {'dist-tags': distTags, time} = await fetchProjectNpmPackageInfo(config); - const today = new Date(); - const active: LtsBranch[] = []; - const inactive: LtsBranch[] = []; - - // Iterate through the NPM package information and determine active/inactive LTS versions with - // their corresponding branches. We assume that an LTS tagged version in NPM belongs to the - // last-minor branch of a given major (i.e. we assume there are no outdated LTS NPM dist tags). - for (const npmDistTag in distTags) { - if (isLtsDistTag(npmDistTag)) { - const version = semver.parse(distTags[npmDistTag])!; - const branchName = `${version.major}.${version.minor}.x`; - const majorReleaseDate = new Date(time[`${version.major}.0.0`]); - const ltsEndDate = computeLtsEndDateOfMajor(majorReleaseDate); - const ltsBranch: LtsBranch = {name: branchName, version, npmDistTag}; - // Depending on whether the LTS phase is still active, add the branch - // to the list of active or inactive LTS branches. - if (today <= ltsEndDate) { - active.push(ltsBranch); - } else { - inactive.push(ltsBranch); - } - } - } - - // Sort LTS branches in descending order. i.e. most recent ones first. - active.sort((a, b) => semver.rcompare(a.version, b.version)); - inactive.sort((a, b) => semver.rcompare(a.version, b.version)); - - return {active, inactive}; -} - -/** Gets whether the specified tag corresponds to a LTS dist tag. */ -export function isLtsDistTag(tagName: string): tagName is LtsNpmDistTag { - return ltsNpmDistTagRegex.test(tagName); -} - -/** - * Computes the date when long-term support ends for a major released at the - * specified date. - */ -export function computeLtsEndDateOfMajor(majorReleaseDate: Date): Date { - return new Date( - majorReleaseDate.getFullYear(), - majorReleaseDate.getMonth() + majorActiveSupportDuration + majorLongTermSupportDuration, - majorReleaseDate.getDate(), majorReleaseDate.getHours(), majorReleaseDate.getMinutes(), - majorReleaseDate.getSeconds(), majorReleaseDate.getMilliseconds()); -} - -/** Gets the long-term support NPM dist tag for a given major version. */ -export function getLtsNpmDistTagOfMajor(major: number): LtsNpmDistTag { - // LTS versions should be tagged in NPM in the following format: `v{major}-lts`. - return `v${major}-lts` as const; -} diff --git a/dev-infra/release/versioning/next-prerelease-version.ts b/dev-infra/release/versioning/next-prerelease-version.ts deleted file mode 100644 index 8603a11d14721b..00000000000000 --- a/dev-infra/release/versioning/next-prerelease-version.ts +++ /dev/null @@ -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 semver from 'semver'; - -import {semverInc} from '../../utils/semver'; -import {ReleaseConfig} from '../config/index'; - -import {ActiveReleaseTrains} from './active-release-trains'; -import {isVersionPublishedToNpm} from './npm-registry'; - -/** Computes the new pre-release version for the next release-train. */ -export async function computeNewPrereleaseVersionForNext( - active: ActiveReleaseTrains, config: ReleaseConfig): Promise { - const {version: nextVersion} = active.next; - const isNextPublishedToNpm = await isVersionPublishedToNpm(nextVersion, config); - // Special-case where the version in the `next` release-train is not published yet. This - // happens when we recently branched off for feature-freeze. We already bump the version to - // the next minor or major, but do not publish immediately. Cutting a release immediately would - // be not helpful as there are no other changes than in the feature-freeze branch. If we happen - // to detect this case, we stage the release as usual but do not increment the version. - if (isNextPublishedToNpm) { - return semverInc(nextVersion, 'prerelease'); - } else { - return nextVersion; - } -} diff --git a/dev-infra/release/versioning/npm-publish.ts b/dev-infra/release/versioning/npm-publish.ts deleted file mode 100644 index 248bef824d32b9..00000000000000 --- a/dev-infra/release/versioning/npm-publish.ts +++ /dev/null @@ -1,95 +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 semver from 'semver'; - -import {spawn, spawnInteractive} from '../../utils/child-process'; - -import {NpmDistTag} from './npm-registry'; - -/** - * Runs NPM publish within a specified package directory. - * @throws With the process log output if the publish failed. - */ -export async function runNpmPublish( - packagePath: string, distTag: NpmDistTag, registryUrl: string|undefined) { - const args = ['publish', '--access', 'public', '--tag', distTag]; - // If a custom registry URL has been specified, add the `--registry` flag. - if (registryUrl !== undefined) { - args.push('--registry', registryUrl); - } - await spawn('npm', args, {cwd: packagePath, mode: 'silent'}); -} - -/** - * Sets the NPM tag to the specified version for the given package. - * @throws With the process log output if the tagging failed. - */ -export async function setNpmTagForPackage( - packageName: string, distTag: string, version: semver.SemVer, registryUrl: string|undefined) { - const args = ['dist-tag', 'add', `${packageName}@${version}`, distTag]; - // If a custom registry URL has been specified, add the `--registry` flag. - if (registryUrl !== undefined) { - args.push('--registry', registryUrl); - } - await spawn('npm', args, {mode: 'silent'}); -} - -/** - * Checks whether the user is currently logged into NPM. - * @returns Whether the user is currently logged into NPM. - */ -export async function npmIsLoggedIn(registryUrl: string|undefined): Promise { - const args = ['whoami']; - // If a custom registry URL has been specified, add the `--registry` flag. - if (registryUrl !== undefined) { - args.push('--registry', registryUrl); - } - try { - await spawn('npm', args, {mode: 'silent'}); - } catch (e) { - return false; - } - return true; -} - -/** - * Log into NPM at a provided registry. - * @throws With the `npm login` status code if the login failed. - */ -export async function npmLogin(registryUrl: string|undefined) { - const args = ['login', '--no-browser']; - // If a custom registry URL has been specified, add the `--registry` flag. The `--registry` flag - // must be spliced into the correct place in the command as npm expects it to be the flag - // immediately following the login subcommand. - if (registryUrl !== undefined) { - args.splice(1, 0, '--registry', registryUrl); - } - // The login command prompts for username, password and other profile information. Hence - // the process needs to be interactive (i.e. respecting current TTYs stdin). - await spawnInteractive('npm', args); -} - -/** - * Log out of NPM at a provided registry. - * @returns Whether the user was logged out of NPM. - */ -export async function npmLogout(registryUrl: string|undefined): Promise { - const args = ['logout']; - // If a custom registry URL has been specified, add the `--registry` flag. The `--registry` flag - // must be spliced into the correct place in the command as npm expects it to be the flag - // immediately following the logout subcommand. - if (registryUrl !== undefined) { - args.splice(1, 0, '--registry', registryUrl); - } - try { - await spawn('npm', args, {mode: 'silent'}); - } finally { - return npmIsLoggedIn(registryUrl); - } -} diff --git a/dev-infra/release/versioning/npm-registry.ts b/dev-infra/release/versioning/npm-registry.ts deleted file mode 100644 index de6daf0970e9e0..00000000000000 --- a/dev-infra/release/versioning/npm-registry.ts +++ /dev/null @@ -1,69 +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 * as semver from 'semver'; - -import {ReleaseConfig} from '../config/index'; -import {LtsNpmDistTag} from './long-term-support'; - -/** Type describing the possible NPM dist tags used by Angular packages. */ -export type NpmDistTag = 'latest'|'next'|LtsNpmDistTag; - -/** Type describing an NPM package fetched from the registry. */ -export interface NpmPackageInfo { - /** Maps of versions and their package JSON objects. */ - 'versions': {[name: string]: undefined|object}; - /** Map of NPM dist-tags and their chosen version. */ - 'dist-tags': {[tagName: string]: string|undefined}; - /** Map of versions and their ISO release time. */ - 'time': {[name: string]: string}; -} - -/** - * Cache for requested NPM package information. A cache is desirable as the NPM - * registry requests are usually very large and slow. - */ -export const _npmPackageInfoCache: {[pkgName: string]: Promise} = {}; - -/** - * Fetches the NPM package representing the project. Angular repositories usually contain - * multiple packages in a monorepo scheme, but packages dealt with as part of the release - * tooling are released together with the same versioning and branching. This means that - * a single package can be used as source of truth for NPM package queries. - */ -export async function fetchProjectNpmPackageInfo(config: ReleaseConfig): Promise { - const pkgName = getRepresentativeNpmPackage(config); - return await fetchPackageInfoFromNpmRegistry(pkgName); -} - -/** Gets whether the given version is published to NPM or not */ -export async function isVersionPublishedToNpm( - version: semver.SemVer, config: ReleaseConfig): Promise { - const {versions} = await fetchProjectNpmPackageInfo(config); - return versions[version.format()] !== undefined; -} - -/** - * Gets the representative NPM package for the specified release configuration. Angular - * repositories usually contain multiple packages in a monorepo scheme, but packages dealt with - * as part of the release tooling are released together with the same versioning and branching. - * This means that a single package can be used as source of truth for NPM package queries. - */ -function getRepresentativeNpmPackage(config: ReleaseConfig) { - return config.npmPackages[0]; -} - -/** Fetches the specified NPM package from the NPM registry. */ -async function fetchPackageInfoFromNpmRegistry(pkgName: string): Promise { - if (_npmPackageInfoCache[pkgName] === undefined) { - _npmPackageInfoCache[pkgName] = - fetch(`https://registry.npmjs.org/${pkgName}`).then(r => r.json()); - } - return await _npmPackageInfoCache[pkgName]; -} diff --git a/dev-infra/release/versioning/print-active-trains.ts b/dev-infra/release/versioning/print-active-trains.ts deleted file mode 100644 index b4314cea2e5dbd..00000000000000 --- a/dev-infra/release/versioning/print-active-trains.ts +++ /dev/null @@ -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 {blue, bold, info} from '../../utils/console'; -import {ReleaseConfig} from '../config/index'; - -import {ActiveReleaseTrains} from './active-release-trains'; -import {fetchLongTermSupportBranchesFromNpm} from './long-term-support'; -import {isVersionPublishedToNpm} from './npm-registry'; - -/** - * Prints the active release trains to the console. - * @params active Active release trains that should be printed. - * @params config Release configuration used for querying NPM on published versions. - */ -export async function printActiveReleaseTrains( - active: ActiveReleaseTrains, config: ReleaseConfig): Promise { - const {releaseCandidate, next, latest} = active; - const isNextPublishedToNpm = await isVersionPublishedToNpm(next.version, config); - const nextTrainType = next.isMajor ? 'major' : 'minor'; - const ltsBranches = await fetchLongTermSupportBranchesFromNpm(config); - - info(); - info(blue('Current version branches in the project:')); - - // Print information for release trains in the feature-freeze/release-candidate phase. - if (releaseCandidate !== null) { - const rcVersion = releaseCandidate.version; - const rcTrainType = releaseCandidate.isMajor ? 'major' : 'minor'; - const rcTrainPhase = - rcVersion.prerelease[0] === 'next' ? 'feature-freeze' : 'release-candidate'; - info( - ` • ${bold(releaseCandidate.branchName)} contains changes for an upcoming ` + - `${rcTrainType} that is currently in ${bold(rcTrainPhase)} phase.`); - info(` Most recent pre-release for this branch is "${bold(`v${rcVersion}`)}".`); - } - - // Print information about the release-train in the latest phase. i.e. the patch branch. - info(` • ${bold(latest.branchName)} contains changes for the most recent patch.`); - info(` Most recent patch version for this branch is "${bold(`v${latest.version}`)}".`); - - // Print information about the release-train in the next phase. - info( - ` • ${bold(next.branchName)} contains changes for a ${nextTrainType} ` + - `currently in active development.`); - // Note that there is a special case for versions in the next release-train. The version in - // the next branch is not always published to NPM. This can happen when we recently branched - // off for a feature-freeze release-train. More details are in the next pre-release action. - if (isNextPublishedToNpm) { - info(` Most recent pre-release version for this branch is "${bold(`v${next.version}`)}".`); - } else { - info( - ` Version is currently set to "${bold(`v${next.version}`)}", but has not been ` + - `published yet.`); - } - - // If no release-train in release-candidate or feature-freeze phase is active, - // we print a message as last bullet point to make this clear. - if (releaseCandidate === null) { - info(' • No release-candidate or feature-freeze branch currently active.'); - } - - info(); - info(blue('Current active LTS version branches:')); - - // Print all active LTS branches (each branch as own bullet point). - if (ltsBranches.active.length !== 0) { - for (const ltsBranch of ltsBranches.active) { - info(` • ${bold(ltsBranch.name)} is currently in active long-term support phase.`); - info(` Most recent patch version for this branch is "${bold(`v${ltsBranch.version}`)}".`); - } - } - - info(); -} diff --git a/dev-infra/release/versioning/release-trains.ts b/dev-infra/release/versioning/release-trains.ts deleted file mode 100644 index ee72c603f67c5a..00000000000000 --- a/dev-infra/release/versioning/release-trains.ts +++ /dev/null @@ -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 semver from 'semver'; - -/** Class describing a release-train. */ -export class ReleaseTrain { - /** Whether the release train is currently targeting a major. */ - isMajor = this.version.minor === 0 && this.version.patch === 0; - - constructor( - /** Name of the branch for this release-train. */ - public branchName: string, - /** Most recent version for this release train. */ - public version: semver.SemVer) {} -} diff --git a/dev-infra/release/versioning/version-branches.ts b/dev-infra/release/versioning/version-branches.ts deleted file mode 100644 index 5f47dc117b5efe..00000000000000 --- a/dev-infra/release/versioning/version-branches.ts +++ /dev/null @@ -1,94 +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 semver from 'semver'; -import {GithubClient, GithubRepo} from '../../utils/git/github'; - -/** Type describing a Github repository with corresponding API client. */ -export interface GithubRepoWithApi extends GithubRepo { - /** API client that can access the repository. */ - api: GithubClient; -} - -/** Type describing a version-branch. */ -export interface VersionBranch { - /** Name of the branch in Git. e.g. `10.0.x`. */ - name: string; - /** - * Parsed SemVer version for the version-branch. Version branches technically do - * not follow the SemVer format, but we can have representative SemVer versions - * that can be used for comparisons, sorting and other checks. - */ - parsed: semver.SemVer; -} - -/** Regular expression that matches version-branches. */ -const versionBranchNameRegex = /^(\d+)\.(\d+)\.x$/; - -/** Gets the version of a given branch by reading the `package.json` upstream. */ -export async function getVersionOfBranch( - repo: GithubRepoWithApi, branchName: string): Promise { - const {data} = await repo.api.repos.getContent( - {owner: repo.owner, repo: repo.name, path: '/package.json', ref: branchName}); - // Workaround for: https://github.com/octokit/rest.js/issues/32. - // TODO: Remove cast once types of Octokit `getContent` are fixed. - const content = (data as {content?: string}).content; - if (!content) { - throw Error(`Unable to read "package.json" file from repository.`); - } - const {version} = JSON.parse(Buffer.from(content, 'base64').toString()) as - {version: string, [key: string]: any}; - const parsedVersion = semver.parse(version); - if (parsedVersion === null) { - throw Error(`Invalid version detected in following branch: ${branchName}.`); - } - return parsedVersion; -} - -/** Whether the given branch corresponds to a version branch. */ -export function isVersionBranch(branchName: string): boolean { - return versionBranchNameRegex.test(branchName); -} - -/** - * Converts a given version-branch into a SemVer version that can be used with SemVer - * utilities. e.g. to determine semantic order, extract major digit, compare. - * - * For example `10.0.x` will become `10.0.0` in SemVer. The patch digit is not - * relevant but needed for parsing. SemVer does not allow `x` as patch digit. - */ -export function getVersionForVersionBranch(branchName: string): semver.SemVer|null { - return semver.parse(branchName.replace(versionBranchNameRegex, '$1.$2.0')); -} - -/** - * Gets the version branches for the specified major versions in descending - * order. i.e. latest version branches first. - */ -export async function getBranchesForMajorVersions( - repo: GithubRepoWithApi, majorVersions: number[]): Promise { - const branchData = await repo.api.paginate( - repo.api.repos.listBranches, {owner: repo.owner, repo: repo.name, protected: true}); - const branches: VersionBranch[] = []; - - for (const {name} of branchData) { - if (!isVersionBranch(name)) { - continue; - } - // Convert the version-branch into a SemVer version that can be used with the - // SemVer utilities. e.g. to determine semantic order, compare versions. - const parsed = getVersionForVersionBranch(name); - // Collect all version-branches that match the specified major versions. - if (parsed !== null && majorVersions.includes(parsed.major)) { - branches.push({name, parsed}); - } - } - - // Sort captured version-branches in descending order. - return branches.sort((a, b) => semver.rcompare(a.parsed, b.parsed)); -} diff --git a/dev-infra/tmpl-package.json b/dev-infra/tmpl-package.json deleted file mode 100644 index 901b3b9d36685d..00000000000000 --- a/dev-infra/tmpl-package.json +++ /dev/null @@ -1,63 +0,0 @@ -{ - "name": "@angular/dev-infra-private", - "version": "0.0.0", - "description": "INTERNAL USE ONLY - Angular internal DevInfra tooling/scripts - INTERNAL USE ONLY", - "license": "MIT", - "private": true, - "bin": { - "ng-dev": "./cli.js", - "ts-circular-deps": "./ts-circular-dependencies/index.js" - }, - "dependencies": { - "@angular/benchpress": "0.2.1", - "@bazel/buildifier": "", - "@bazel/runfiles": "", - "@microsoft/api-extractor": "", - "@octokit/graphql": "", - "@octokit/rest": "", - "@octokit/types": "", - "brotli": "", - "chalk": "", - "clang-format": "", - "cli-progress": "", - "conventional-commits-parser": "", - "ejs": "", - "git-raw-commits": "", - "glob": "", - "inquirer": "", - "minimatch": "", - "multimatch": "", - "node-fetch": "", - "node-uuid": "", - "ora": "", - "protractor": "", - "selenium-webdriver": "", - "semver": "", - "ts-node": "", - "tslib": "", - "typed-graphqlify": "", - "typescript": "", - "yaml": "", - "yargs": "" - }, - "peerDependenciesMeta": { - "@bazel/buildifier": { - "optional": true - }, - "clang-format": { - "optional": true - }, - "prettier": { - "optional": true - }, - "protractor": { - "optional": true - }, - "selenium-webdriver": { - "optional": true - }, - "ts-node": { - "optional": true - } - } -} diff --git a/dev-infra/ts-circular-dependencies/BUILD.bazel b/dev-infra/ts-circular-dependencies/BUILD.bazel deleted file mode 100644 index 13fbc819e06cc7..00000000000000 --- a/dev-infra/ts-circular-dependencies/BUILD.bazel +++ /dev/null @@ -1,15 +0,0 @@ -load("//dev-infra:defaults.bzl", "ts_library") - -ts_library( - name = "ts-circular-dependencies", - srcs = glob(["*.ts"]), - visibility = ["//dev-infra:__subpackages__"], - deps = [ - "//dev-infra/utils", - "@npm//@types/glob", - "@npm//@types/node", - "@npm//@types/yargs", - "@npm//chalk", - "@npm//typescript", - ], -) diff --git a/dev-infra/ts-circular-dependencies/README.md b/dev-infra/ts-circular-dependencies/README.md deleted file mode 100644 index 2ed88cf1ba3c80..00000000000000 --- a/dev-infra/ts-circular-dependencies/README.md +++ /dev/null @@ -1,82 +0,0 @@ -### ts-circular-dependencies - -This tool requires a test configuration that declares a set of source files which -should be checked for cyclic dependencies. e.g. - -``` -yarn ts-circular-deps --config ./test-config.js -``` - -### Limitations - -In order to detect cycles, the tool currently visits each source file and runs -depth first search. If the DFS comes across any node that is part of the current -DFS path, then a cycle has been detected and the tool will capture it. - -This algorithm has limitations. For example, consider the following graph: - -![Example graph](./example-graph.png) - -Depending on which source file is considered first, the output of the circular dependency tool -will be different. This is because the tool does not recursively find _all_ possible cycles. This -would be too inefficient for large graphs (especially in the `angular/angular` repository). - -In this concrete example, the tool will visit `r3_test_bed` first. Then the first neighbour -(based on the import in the source file) will be visited. This is `test_bed`. Once done, the -tool will visit the first neighbour of `test_bed`. This is `r3_test_bed` again. The node has -already been visited, and also is part of the current DFS path. The tool captures this as cycle. - -As no more nodes can be visited within that path, the tool continues (as per DFS algorithm) -with visiting the remaining neighbours of `r3_test_bed`. It will visit `test_bed_common` and -then come across `test_bed`. The tool only knows that `test_bed` has already been visited, but -it does not know that it would close a cycle. The tool certainly could know this by recursively -checking neighbours of `test_bed` again, but this is inefficient and will cause the algorithm -to eventually degenerate into brute-force. - -In summary, the tool is unable to capture _all_ elementary cycles in the graph. This does not -mean though that the tool is incorrectly suggesting that there are _no_ cycles in a graph. The -tool is still able to correctly detect whether there are _any_ cycles in a graph or not. For -example, if edge from `r3_test_bed` to `test_bed` is removed, then the tool will be able to -capture at least one of the other cycles. The golden will change in an unexpected way, but it's -**expected** given the trade-off we take for an acceptable running time. - -Other algorithms exist which are proven to print out _all_ the elementary cycles in a directed -graph. For example: - -* [Johnson's algorithm for finding simple cycles][johnson-cycles]. -* [Tarjan's algorithm for enumerating elementary circuits][tarjan-cycles]. - -Experiments with these algorithms unveiled that usual source file graphs we have in Angular -repositories are too large to be processed in acceptable time. At the time of writing, the -source file graph of `angular/angular` consists of 3350 nodes and 8730 edges. - -Algorithms like the one from Donald B. Johnson, which first split the graph into strongly -connected components, and then search for elementary cycles in all components with at least -two vertices, are too inefficient for the source files graphs we have. Time complexity for -such algorithms is described to be `O((n + e)(c + 1))` where `c` is the number of elementary -circuits. Donald B. Johnson describes the number of elementary circuits the followed: - -> Thus the number of elementary circuits in a directed graph can grow faster with n than -the exponential 2" - -This shows quite well that these algorithms become quickly inefficient the more vertices, edges -and simple cycles a graph has. Finding elementary cycles of arbitrary length seems NP-complete as -finding a Hamiltonian cycle with length of `n` is NP-complete too. Below is a quote from a -[paper describing a randomized algorithm](np-complete-cycles) for finding simple cycles of a -_fixed_ length that seems to confirm this hypothesis: - -> It is well known that finding the longest cycle in a graph is a hard problem, since finding -a hamiltonian cycle is NP-complete. Hence finding a simple cycle of length k, for an arbitrary -k, is NP-complete. - -Other tools like `madge` or `dpdm` have the same limitations. - -**Resources**: - -* [Finding all the elementary circuits of a directed graph - Donald. B. Johnson][johnson-cycles] -* [Enumeration of the elementary circuits of a directed graph - Robert Tarjan][tarjan-cycles] -* [Once again: Finding simple cycles in graphs - Carsten Dorgerlohx; Jürgen Wirtgen][np-complete-cycles] - -[johnson-cycles]: https://www.cs.tufts.edu/comp/150GA/homeworks/hw1/Johnson%2075.PDF -[tarjan-cycles]: https://ecommons.cornell.edu/bitstream/handle/1813/5941/72-145.pdf?sequence=1&isAllowed=y -[np-complete-cycles]: https://pdfs.semanticscholar.org/16b2/d1a3cf4a8a5dbcad10bb901724631ebead33.pdf diff --git a/dev-infra/ts-circular-dependencies/analyzer.ts b/dev-infra/ts-circular-dependencies/analyzer.ts deleted file mode 100644 index a526eb622c91ff..00000000000000 --- a/dev-infra/ts-circular-dependencies/analyzer.ts +++ /dev/null @@ -1,133 +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 {dirname, join, resolve} from 'path'; -import * as ts from 'typescript'; - -import {getFileStatus} from './file_system'; -import {getModuleReferences} from './parser'; - -export type ModuleResolver = (specifier: string) => string|null; - -/** - * Reference chains describe a sequence of source files which are connected through imports. - * e.g. `file_a.ts` imports `file_b.ts`, whereas `file_b.ts` imports `file_c.ts`. The reference - * chain data structure could be used to represent this import sequence. - */ -export type ReferenceChain = T[]; - -/** Default extensions that the analyzer uses for resolving imports. */ -const DEFAULT_EXTENSIONS = ['ts', 'js', 'd.ts']; - -/** - * Analyzer that can be used to detect import cycles within source files. It supports - * custom module resolution, source file caching and collects unresolved specifiers. - */ -export class Analyzer { - private _sourceFileCache = new Map(); - - unresolvedModules = new Set(); - unresolvedFiles = new Map(); - - constructor( - public resolveModuleFn?: ModuleResolver, public extensions: string[] = DEFAULT_EXTENSIONS) {} - - /** Finds all cycles in the specified source file. */ - findCycles(sf: ts.SourceFile, visited = new WeakSet(), path: ReferenceChain = []): - ReferenceChain[] { - const previousIndex = path.indexOf(sf); - // If the given node is already part of the current path, then a cycle has - // been found. Add the reference chain which represents the cycle to the results. - if (previousIndex !== -1) { - return [path.slice(previousIndex)]; - } - // If the node has already been visited, then it's not necessary to go check its edges - // again. Cycles would have been already detected and collected in the first check. - if (visited.has(sf)) { - return []; - } - path.push(sf); - visited.add(sf); - // Go through all edges, which are determined through import/exports, and collect cycles. - const result: ReferenceChain[] = []; - for (const ref of getModuleReferences(sf)) { - const targetFile = this._resolveImport(ref, sf.fileName); - if (targetFile !== null) { - result.push(...this.findCycles(this.getSourceFile(targetFile), visited, path.slice())); - } - } - return result; - } - - /** Gets the TypeScript source file of the specified path. */ - getSourceFile(filePath: string): ts.SourceFile { - const resolvedPath = resolve(filePath); - if (this._sourceFileCache.has(resolvedPath)) { - return this._sourceFileCache.get(resolvedPath)!; - } - const fileContent = readFileSync(resolvedPath, 'utf8'); - const sourceFile = - ts.createSourceFile(resolvedPath, fileContent, ts.ScriptTarget.Latest, false); - this._sourceFileCache.set(resolvedPath, sourceFile); - return sourceFile; - } - - /** Resolves the given import specifier with respect to the specified containing file path. */ - private _resolveImport(specifier: string, containingFilePath: string): string|null { - if (specifier.charAt(0) === '.') { - const resolvedPath = this._resolveFileSpecifier(specifier, containingFilePath); - if (resolvedPath === null) { - this._trackUnresolvedFileImport(specifier, containingFilePath); - } - return resolvedPath; - } - if (this.resolveModuleFn) { - const targetFile = this.resolveModuleFn(specifier); - if (targetFile !== null) { - const resolvedPath = this._resolveFileSpecifier(targetFile); - if (resolvedPath !== null) { - return resolvedPath; - } - } - } - this.unresolvedModules.add(specifier); - return null; - } - - /** Tracks the given file import as unresolved. */ - private _trackUnresolvedFileImport(specifier: string, originFilePath: string) { - if (!this.unresolvedFiles.has(originFilePath)) { - this.unresolvedFiles.set(originFilePath, [specifier]); - } - this.unresolvedFiles.get(originFilePath)!.push(specifier); - } - - /** Resolves the given import specifier to the corresponding source file. */ - private _resolveFileSpecifier(specifier: string, containingFilePath?: string): string|null { - const importFullPath = - containingFilePath !== undefined ? join(dirname(containingFilePath), specifier) : specifier; - const stat = getFileStatus(importFullPath); - if (stat && stat.isFile()) { - return importFullPath; - } - for (const extension of this.extensions) { - const pathWithExtension = `${importFullPath}.${extension}`; - const stat = getFileStatus(pathWithExtension); - if (stat && stat.isFile()) { - return pathWithExtension; - } - } - // Directories should be considered last. TypeScript first looks for source files, then - // falls back to directories if no file with appropriate extension could be found. - if (stat && stat.isDirectory()) { - return this._resolveFileSpecifier(join(importFullPath, 'index')); - } - return null; - } -} diff --git a/dev-infra/ts-circular-dependencies/config.ts b/dev-infra/ts-circular-dependencies/config.ts deleted file mode 100644 index 86c7d1f1d05fda..00000000000000 --- a/dev-infra/ts-circular-dependencies/config.ts +++ /dev/null @@ -1,61 +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 {dirname, isAbsolute, resolve} from 'path'; - -import {error} from '../utils/console'; - -import {ModuleResolver} from './analyzer'; - - -/** Configuration for a circular dependencies test. */ -export interface CircularDependenciesTestConfig { - /** Base directory used for shortening paths in the golden file. */ - baseDir: string; - /** Path to the golden file that is used for checking and approving. */ - goldenFile: string; - /** Glob that resolves source files which should be checked. */ - glob: string; - /** - * Optional module resolver function that can be used to resolve modules - * to absolute file paths. - */ - resolveModule?: ModuleResolver; - /** - * Optional command that will be displayed if the golden check failed. This can be used - * to consistently use script aliases for checking/approving the golden. - */ - approveCommand?: string; -} - -/** - * Loads the configuration for the circular dependencies test. If the config cannot be - * loaded, an error will be printed and the process exists with a non-zero exit code. - */ -export function loadTestConfig(configPath: string): CircularDependenciesTestConfig { - const configBaseDir = dirname(configPath); - const resolveRelativePath = (relativePath: string) => resolve(configBaseDir, relativePath); - - try { - const config = require(configPath) as CircularDependenciesTestConfig; - if (!isAbsolute(config.baseDir)) { - config.baseDir = resolveRelativePath(config.baseDir); - } - if (!isAbsolute(config.goldenFile)) { - config.goldenFile = resolveRelativePath(config.goldenFile); - } - if (!isAbsolute(config.glob)) { - config.glob = resolveRelativePath(config.glob); - } - return config; - } catch (e) { - error('Could not load test configuration file at: ' + configPath); - error(`Failed with: ${e.message}`); - process.exit(1); - } -} diff --git a/dev-infra/ts-circular-dependencies/example-graph.png b/dev-infra/ts-circular-dependencies/example-graph.png deleted file mode 100644 index 02adffbb86b0adeae617ce005eeef4415968712d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 41607 zcmZ5|1yod9`!}7^Ehr_8gh(qTp(x!_LnDo}d%GYm6U|Ng!8RrLCV~wqAh0^HP2sqEfy0)LkE;9 zgop(A|9R!RBK6+|Kdh;~2ths=|9!=GLGL5|pEu(p%7`tC;rafbw*~ohK~w*8MflaB zw*(QZVhVo$-y7M{-=Rs0q5St{EEsgfQ|`E_tljKtA0?_<-PrgpMqzuRgy=?exJswyf*x{Z#(o5KbetA`yXYc%E%$n4Xy~kJ+1s1bM$_Qs&o*20^6o2JDZhu-nm-0~BUK)L z7joKKkwh`f)F{)d?YV0?wN+HM=XIgb8-CBBxlx|zKTGt52{G7@__5hwYKq;#en7h~ zD)z_%_FBKNx|-JeLe|q!Z=uH@FNM!^)naP&1j`kY#3OOz+YAHxwOZ#qx72SOy~~0w zg<;oNNp*&9(mBtlP`Z7)o=HzRM*Lmyl@zcq31AHd7rbw;r`D?mMI18IOG`Nz4*2?$ zw9{fAmT2_FPBfXkJQWsgsxyCcFOlCxBHa`9a*H0vSHkdHWe%VH=B6NEeqFiH( z>y+JR9P7S^b=6R0^v1NBni@B6T1WA}tDA3(M8br-9DtCy6w2B9{OnG(&BENfy0bU! z)KpjWX!uBXZekMUAzPIu_L&ARvf3y=8UN@;lIZY$U28Rcg0zW6M}ze5OEClG+iNOg zdKA8V8*9os@UOMUZ+8(?eYgO^Tj}uUABj$T%x8U&cOQUolOx zCaq$3?HFP=+Le0p3XV!9fR#*tidSO*1boBB#P;KHwq`c zS`T}ICrpajwWHiTl9rB;VJ^aX+JtP@eZR%JX}6ZGB&4wVKQ}3f#K4Iwa&u;MIp}@! z6Qv_xDN9|vPQ6Hd;BLA7_TAGcg&NaOp=UuPoJG@paks`=c4+~7E;_Nk!5c5!cWdG< zcj|wJPgI!=E|Vw<2LFk1u`bN$WPyVTD4D*;u%6es$>r7YASG=NgZXtv^N zb$b~74F+kg>uB7=s*j`(tNBUU%lXOJ%c+-(u`>ooD+o1H}E%NNTwn8~+A{WU||vpr~IWB&f~)3HL(RU90?cLpsj+WPw_pV6_&dJ0wN3uAVlIOgv*tfvm3 zzUk+kWdF5Qc=dO!BVxs}yAZe0Q{C1mn6UJ`uYYO&uqWvh;wrE0K^w2FkjJ7;g!OFs z>|idvJ<;=QA-a>by{#*trW{(xI_t11gxV#MCgNGyRzL8!_(2J2@EZjnpSimbG9fEp zBDsw{wQH+0tY@doop+7zF+KSUt@@taRiFE+B|@t@pY9dn)@MIZ zB(@i;SK`Dn9ZcQDl8nxHI$IMr8*zW=6T?yo=54CBnaJ93Af*jC5$5h}9p`jo!P1}g zk9m=vzPuErAY!kM(_41GJZJS*mWEJmxOms8L9<3K@G@7CJX zav+*UlhcG&uNFZ$qvP&O&3B99tv}fq%?9-i^1~Co8oIfdY!gx{cQomWciu2Zcie#R&Xfpz8}$7a%Y;M}%U;pDyG@#xRv(nWY9xe=l7Hy?-VzWvI;!Ol|P?PMvO{Zk>+=R;q#WcmokE4{|C zV1M`Z@3jqVQk}s}sT-0KgQ-v1aNcjR{i$L|C`WX-rS;2LOm|2*^>zw9tkwr$NgTQ* zDeR~W2N?N50gMOd%iYe~-zi{M&2Ah;zb5q;2Sjf@=&uU?`+&h>QC%HuWzEYvL3-~t z88q_bv?r%t=r&C)@mXi7%EU;a(1|Z~2s-SAmNYTt!#hda)JBoQ6gF z`;KUk1VjBm(;EyISPmlZ(0AH2WOCXp{LpvuDp(8)X^}%~xfnxXFl9S@2I7aav*5m8 zgNTe4l=OG8{pD$x?8S)bBp!dNEi7@$glx~u46$V)1&I~lyEc~&Xb~_z)ZJo758B%y^-@I_2eyYlPfgN= zJ@%Y_n&&W~kqrzS!vxJh@FlV%X}%<-4=h6`~~*Y)8uo#SXEYE=WH4Gqd;*QF5VhYu!G`JmJ0i&*4vDQ_Ui_jq!gYDw{q+ovQiQ zgmx{6n8t0LxvIQ-V;M$`ma%iEWt^Ijk|h4Bhn9C`bR=kWcsvv5h23ij3Y{b;ZPa7( zW_aVLvz#1?bG001n>;U{=<-(e5!2ZQ`+c9v6W)q!6m0W3 z>3`CkAL&hZN5=d67I-3O=}E!Y+G(wBef6pwCu4I$!Y)U~!VNDqgbOS+)*C;4>?&ebj&=5^J9UyOnvaJf@;>|&t%;xD`%!GF$zd*Hs{G^sn! zu(vTg2|87QKsw%Ff{}(onyL5Rys+)BTZ==(Ci5*~vW!5fs%~QRfN$E%G~Q)Bv%j!+ z>Z!fTMLaL2K&Y-|lC%5=B5-456QfV+x}>DIttBwDU?luPksNU|AD|L$(dM5`Z=BhS z=k41|;hBY5FD}S$CwgRXuj`3871oqk*qAwQ+ms~n&f6U9G&jKFD0mD*Q~dY^^_b5# zUfd=K4~l3vf007CJCa*+Z@eMum(_6P@Nq}Q>GxEV+f?DnEfC8Rri#`ja$2?D!}sT= zrFOqE?psl}y4nv_NnyL!+l0^axYue|RankC5AYVb^!2R5&W%+x1`H67HZI<+A9X_- zrK(Ih@c33Q+rCE0zU>!}5Daa?#J&WGYz9|&Ct`PZ=A2BrwH!D)%=F9`4=-Eo?ozo)`8ljjO-Y6B?Cj0!5~q6PNa5f(uT~cXs?jB^`h2Lk;j_%Ww@?uUpio7QDQsu1 zVIZWV{cbmp@1YRU>L*Vp=; zD80LTf2Gws7ErRM&6l{dFL*aLI~3rpyV@h{;}mHNmugQ=G*Wn>P+&I+2Hxs6yyn~< z&J5^doo%d)ziZhsUu8B;?OwXZjw({&lbo+I*Q7kNPD9|Zw@#h=%9$WENGJQj@8U#* z)J+qiO7%TOcv8sm><#I~=U)??(pp%bdNWDbc0%5a>KN0Qmf-CSac^cWFSQG5#vAi2 zG>{6+BcyzolAh$OK6XDT=j+ETzG!>=_QpflZTaKgt)>KDQGvbTkAwz?-5I@>c}8Oq z?g(j=-Jx8$;VwicP{287(=V8c0u}bhCWT$7ow;ydzpN}_vt*P{dOTcfJ=&v%<0|5} z*^i37<4^6)>}Jp9bx3!4?y@*ds%>Rp)D!mgm(r-)e}a|-n!OAe%U7x$l^32Kw6NSQ zZN08+G>^u`#qEk}!0(Q8dGpL~c9%@N!{%#!!O}a&af_Bu7vIfP)>W5%nmJO0#&3A* zICdGzcE|f*=93G}ko2%Tr)^VY^}X?k4Wv!~7c;M-s3L5{AC-#5$t7*EQ;D#%cRoyN z+*>GZsUp96GxNA9Uq(T={%4_q(@VEBKi6tXFOh}oq71}@y#FkA0F&ZPte=m!ckXh# zk=gT}u&ZvBJOOA#*%yfdw2cLvLffKPSkvCqN3T)(+b3FnK+hZYS%^`Av%I%Ui{>9tFWa;ICLVeP@zt z#ZVB#5X;7&FVj*(xZ>p`qo@q{L@zY2U+N`unLnNFZKS?F%1a&`Oj_3ot4Dj&TWh@= zU8-B+-QLag%HAiGs8mXi3J$||3zOgIuCwd!4Y;@?JitWM=2#Z9#7(?P-259;g80m{ z=2EFrD&2^ahc?3gPrDg%3s}YxJV!=yoM9_3Y}BtC^;&e#Myv?c{h8J#rai07gd>NF zOkVOwzz7mDX>gmhhsW5r?-HZ0?x|;+OHDudC8Ykl)YIsreDg~4^9Vu~rM!+?@U+T% zV)Z=6!bilm8%x;xo3R-x&4Ues6d?v3vFqzk41AvFm1>>77wCS^U)A5OaE3C=nK^Eo zK1|}R;_B{LG##j>G=254|H4dr@>h{|?^HX(uRf{66x)@r4q)sU6;G|sI&FJ!sz$y8J;F(n2_Y3{0tUT>! z_+elDoP3_*<8BNMUsPMYv1QkUY^!OMDc25aNW*A7^7Y#ySod?sSmu3=+w=fy$% zot}un8S}NW*%)t9%Gda&kGs5715#pA^akR239C^My;c=|z`xot*6=44vv zC6b$ZaeNI$M!TwiRzhO;{s_D2 z_K`!N?nssyefUmygq&_gd@_$)rDwBwhV^9bhpN)EW=$7y$AZvYd3%r2&GEkAJmr@% z^6^W2drdbN+q>1H@@zHBMa_SnnBf&oiNM)L)<>&Z>)MKo0PKhQBB$NU-RUP$QrNql zEGl7ICqM29)eE7R-TK9%nr`9kP7MP*ykG$cGalgb=%#}eM#3)6$is7i{lS*oWb4lP zg_S>cg{L|LNt})I1}dO??%3P94cE)dOKP~gK`-U%rq--=LW5+lZV%*Birqs=IT}ai zt4tgE7d5A-bebYK_I38~wh<4adcSoJ^yfM6u5~Wasq4jyE&`x1ikTrvRaL-dLaD27 zyDpz#RXpfJP1#vdaMuuP8%bCp3LnG6#XL`*Ldx@7&ceA!Z-0%w6vN9(UkZzC%nM#g%I3tD}KjjvGzqXE*N-4eR1M z*s#}{9vv6&EVSx&zC}6yQeApO#g2O?J1tFg@Y-^{)_Q&>ep!AEbTpE(w&9X zPR1w)I2O_abV8a!kr6FxDT@NRX~JkSu~O4SZ$HqU<^UIC?&6n*6q2o=K61HkO+r78iR zwCEZsi>_|Y=y<4OifB_CJd|xEOQU$V5BN@cyl#W zNj>*z&(nFcdqR%PrUcRd5iW`)0OM>|1B>@e7&c`zuP{>9=x9%up@w>g(DRjdXO?Nw z-fmqZPJY2xShX+pK0jU`AAP1!MVUC4szvQ&e)dl{=P5H^yUJ{2yeC5sMM}ZXgFA&q zzc)Zk5_5$eHy_Uu55xZ5Zp~!l7J)`;M6%y(^#^k&HFg4wkL>_C3Rrf8ne-J~PL^^= z9t084cd|vi{lnn!p#n&9gy=V~*;QLtKE2#YaapjxH5@HpblkMq#VQx|*-OWWzKh8I zN)tOZ?Q}Ht&U>o2K}2XF&s8m4lvhdqM+BiqOc7-E2)W&%WWrFpZW0Xvi=FrF(yDL` z;~{JNc>WbN$vBrblGP4OQ#WOH!)EVjbs|rJS{SHIyBcj5YPE4A|AEbl#Bd3twVJc- zs*(t^tobP9a`N5yxHIH^nxw=Bc1)>;f^QFW;F5@a=F)JZAfNmBTaEavaG`Hn*b{p0 zyLQ-lbAP!p4Jx!b6gdqP`a`@Xpzi4!(-UH=#m4L{XOriLk_yg|7sa2VjW%3vNNE;{*>%2Dr?=k8P7B6i+{c`k|{NIp6@{y z5!-&&9lA3Ngdfd$D($(UrEoLd4os$G2+#yxy0_x~N5RX-XRa4GAo1MDc%S^-^{|_4 zzOAv&+`zro()ROAO{lT?VE!CU41FVtUZUurUNZmU&C@!L+V$>8nfsfP9ZMa4S=x3n zuYR|2F_cgE%!vXA=c0#QM1g(7JcCb&*a+=!2{1p;)H=kX{+8S-(^~_f3KQefFc2QbO{Fshdc6M zEJ|T!N6=wT1BjM7EW|6$V`E9h#}4#M^MQKFqJyNxI*_>%OO08Vw(FgEqwtE|x}~en8Y3b>5HobBTRft#`pu}J1nq`%wU<*3AU`$G8eYEq+7C|3)mcFz@;SQ2)Ug~j zr(4_BC!O!$9fBy1!--^TD#MQ#umi0s(-P&5NP3x>aN#}884``nzd-|GfIbiQThgWA zw@D_#y1hAVGy0);o{BS$HZe98TT@djXHEIgbRsL62QCTAV>D~)+XDPO3zvjT$8&2Z z0(a`O94Ka77ClQ$`eI>o4ucff;ZZqo0Siw8H;6I(04xbVFCB%;=8U+1F zfP{%rjfTEkcWW9KEsR&`V>zqUV(y1aM-|sGuC%-_>(}$27{G^CK%gyJZS7kcjo3;D zCABl(q$p+#-rq~^Q;g%#{WO6d+A$!!pL}u&Vs*xO(bHwt_2&orK}l5N-}((cqC)zE zF7d0~ciSa(J2`QBA;AF*Z1bW5xMbP)W~!Ye4E!d>CrTUmbc$AbQ@XoAyE=;^*DPUdCpzAVU^T{?~i{m$%RYXFO zRi;8|;LvNH+D%y!HR=tw_HTikIc<$I7aDc7HznQ|1$G4t2gYlPU7Vzxe0&csJq`^o zt%eoa2DMt7hhqY9?^ystBPr<@wSXN2w;@}^WTDv&mUes9iz{-rl#RY%-+ND<{bXmF zXa9WL_wND^5yL%?Wj0Z_BV#6Fpu;=4-(H{2Gl|q#FU$q||I*f&MZ1!GQf*^h$^Cd$ ziFKjj7-(UKq3R(%F@`>E8eJqAa!Gu(tFhXRc!mfDin_YG&Ay2gwC)wGI$r0in8z(U zm3_&ItfkLKI%0N0KQDGfy*mdEemHy>8Dn3ou7LNKtSGRV z9YuJ*>sBkzN7Fo`h9|Pog*EKo)}V?74Z%m3Be;`1nb=D?d?59w3+~qEN2`1;dvn!h{ANTYg{<3(YOBDre7|Pz zjowyND7*RXF2&GFzQ;;}|?5YCYw@{J%{EI%Pa5CR>_LkshAw-=^2 zekK;@QN?_Zpm6V>B9F&ww#dPLR^0O@1|4pprN9J%MN5?B?oRYwck4M^MOGf1*}sjA&7D z05H-Wfj7V!11pue(s7x0VnYwH z_$!{`BN3u&K}>vV*Hhga5ji^{Q{@J41{ue;fO2bSh}aSFb@3(g;n}FEvOltgg~c4r zqBr>t(*-44pO#sQ2`JvMovJ~hG`E~9f-_*%%nPBWb}MB`m%RPqkj9O-@p~3vK(JSf z1bA(q+Fm*RSu1D=EscG|kCL9d?Zy1DK9*%I69WQ2!|n`z-{qQ%J!99d{SML>S$L=X zvsLG}lniv7yT(A;gzUI`>l{$sioRt!k^`Q)v}n}5&|B|ZOc4zKTI<;vAgmct+SD85 z=55upW6(S%!1MqT`HkAZlcO*6M#CTRt7ZR|fQWFPE^#3VwzfrJi|!_vrB)b)3-;_a z9}nCaw74*!_BzXeb0R*of#j)fj^+c2rj5+dM6W|6Jt&mLlIH@pyA4a+00tGJ!)Ole z!BHQla8c*DhJFhcKFsZa<2j{mbA``;q)rL2-V{&By)Q=7?zrG&t~>$FS)E<KZ#j2>J_N}P4^pi&AXg5>s+CkCVC#GtyH|Oh=DGP1h zxA9U2KsfD(jR@)eZv5$Cn9r%Q8yfBuJI`A4^Ot+BM9PGn9d|#+@H6`Ol+QrDp2E8I ze65>VI_fC?seA2g{0`8QYv{uTI`@EM$*8Nqv@Y!!oeC1X z+)uU)Tjp>)op)hsIRUP?Y4J1WoRh`dn@K4*i7_tbZo({L93=35-B-+4my5SHs`|SFce?i`f3Ge>6YH%?GEaiI$}mV1!_HLfiJK`^9D+_Wft6sJ)-x<0+7*KGKw& zOHB1R`ufS8wXE&t9odf;1Yw!(+n^8<5@E4h-P~O6Q<6A?M)6ULB&24Kv9Vh9(ZA`> ztN^wbbFWC>tX*Tr%x*{=o#bSX{*GpIQm|AyF&znmUk-*es%O}d*WqxMOfO8;6*Sqq zy3{Vhwn|grxsFonIGBG|H+)=hvjTg?JT$)8NpjZ!aMS@?r8(UNP28s&;WYde;qa&U zj)&dE)EyhZFRa?y+EOl-?T&F9$$s&n7z>+}J&1c_zRBf#rVk{)q@+YZq_F(oUJ#89 zE?qC|Hl`%gkS}!hrK(JvRweW4osm@y*2s`gD!DRM^Wm$Il2kHP0!sX~{8 z0)Y5mcfy)S_^(+mhDsD6fHCLMHjobHL;@{`r75jd%Vp@N@1t!#V$XRl^>vFd4B`mj z{}hOv-$&-x9tIqMZDRa8 znjs!rGc}f0DM*GHDtYqV%%6__Kx23pZH@RBt-)Ks3T6S|(S#u=#xR2cR0Ems~72=_|lkdDaa zCH?UwBLoqHk$En+^=~cO&iiexKJZR(v1728vr8IIq;+>WIXMaHwno#FFcckddY`|9 zGmsM7#a0*yEbTg*`L~8yQJD&mtFuGyA|xC4jxl(et3iDb{+6vD`1!8Dw zP_|8#{_VviY73G7LzCJk#l8co+G=jD{q z$7v|~ee)84rx?IE&6A8|kbv~Qvt#83pYIT4V@PkNCCLgH9xiu($fTBw=b}}&<{LUD zMfHMhPnHozsbB|GLW5)=)|5OMRZ+On91h0OzZRax|N@p)E zeD;TBJb{J~qw9PAigBP#6mlxmCjVag#el)ti1D_hZeeD@>(W{yOfocP)wfDsz)ayvM{XdOF%SYF-ukJfoPa*b z;sIj6qLSZoqFCu93(Q00WpCwl{_Ba0abv0`3%G(6nA1b9FIkc&Ha#I~YwHBjxh>(w z_>%o~PfIR<_uU?R68)h8U&j)<3zYEfGddO5w20u~0rci&dSC0>X;TpRmTqv#=6|mn zne@yqM4pj)q93ge!%}1vNg)9-n&!d!C-14f%;7@p?ic!xcZ!%J`}xT`Hb!1@)_}a^ ztTa*U$b1Ru38MfTOdvFwSE}2@7viP*0_jPk%PWTe&9GraiAe<`OBg?DpL^T)FlF4w zoXLQpjpp6KQis;A6TZ6J=Wk?_bUI<`#hMuy(gySXkNV;;Oh9i0b4R>|_XxhjSrijF z-;8)d2``J$zK1OW2IxFx@IF8-`qP)w zwIthG0(xm?U!@mwB_pgWXNNzH)KOtiF>`OOFMErzFjRlzrF;}55_(*6AJKz$WapD% znOVT4NVZyjk%px>&wSmjN{>zzF8G8e^1vc~yZ@L4W zn(DQdEXz*_3Oy-nmCb`!b>+&2d6jF@2$Rn^Nx>SAZqRAcf~42Bs*tjw_yZM!XyCL1 zz?-6$gHCMcuMBZh=j&o3opjFQR z*nqFDB+Iu5LCFH+ds7_sOTM2Cm1=?(^j9r0#HqO#wa~|D9p%}LV^N2mn2yDkwkn6N~L$SEB0G2*PLln7?9j( zxZa&?#H)4ezun`!tV+9PH&vs-fc{pBBvZt+Lj3^9n%Df&_?0nf3P9AKne+ZJL9zbh zkl{=zSQn8({fDXOw@gd>4^N}- z(%HMkb#+sSP}96`8s>pa>I`5=K=-J{Z#4)-^G+ihJpP zxRqeL1-!{7VMm7P|{zFm1x(2J|+ZiDqVS_^53Fe&I+w0M)$t@Nxgg8 zcH1Il5XDWG0O6&P7zTrV+jLAUt8oZQ>1bds#<{H(K>D3dY|ihtXpM;&#PRJfIa`RU zSi>m!OO^L4)F;y=ypX60nfq>CxK9q+75BN_ z*|;=wUa^O$v)(~;PBg`rQjVYM%A zlhe`KztpJzL7`B5_+;qW#dg{Jou?d62A>>$ZwS{fgJeRFd$8UH5Ys<0BFIA?#|<#blSR-+6^a;(oU=?wQ_T{K?B7YEp<@6z zv5h@;`kW+x!D1h3j5=->d8}N zC1(j!u|i@IXwJ*H#`(%c*Pl4EHch*CT(V*xY6t3b#fYKMNx~a9SQCsuL5H<2S()cc zkdv(Nd&1KBE36^^@E9=>%;;u;DPZR87UZQ?CfPK8l@241RN&Ln-v(J8TH*pRQ@NUt z$rOjccBXq?Z2+(Xk)g8ppLZC1I?Nm|ON&InZ~)Lkw@yUQ2g=F(=pE_3Q`LwfJxSa2 z5C-_(3KBBEu@{rXopYW#*!uZVv=%^!wIEv2JOYAsP~aSs$|^9El&dj30Ew&xQ(v=J zU?K(tdkxQk0R3xJ)i;tfnJ7IUsi~`2xef_lQCgLYTPYoLH0=-$-_6(jU&JTCKVymi zsZm5Pg!6lsgs9M0!1Cv8+OD02LI3^C9)2V*vD9{ZU6yoce7FkgZ4rj_J7r0lmq`BT zux;WqK`>1_MKV)oOI^iWj;Q_d{?p3lMqILg6kUuYv9BS1K-X?K$cbWwgTi2{ll5j~O|6WnHdj1|)e_{o&iB+%HT)dN61$TiDn`(FoINrB%# z?X$osxVwVp3JgQYLWi+~IBCPOay6s_$%tSPu4wmf3h|-9)1S|jw?c@ZBZw=!0F4dE zU2$-&V=oH~iA1C#?33HbETF!DH~*;DfNGf^^LF0(S00e`6MGTj2b9Kz^AJ38uJ-<$oGAd<)fqIuRUGM3GP=MU zFnYfOzy~+!3$_?sD15FsxG?~mOvr_6uG52V;PT+d=lK?fig5pkd^jhRqvx_;?gA@! zad9z)dU>)r`lV1&MP;=5yJPMD5=9+FR-Ve^`q!il?W22WKVG_@?xm;&9D8z6mi>tJ zO-K&-a#qe3LyG7;v0&SFovck@aF@V6C=kd+6W4bbB{#WysZ?H*sJ@Y+=|l-SS$l|8 zPXlgQb6{JZ@PDQZ2xtQZrM8OPTxJEt z1xLdQVG}@t9~8qt{Y%o`xj#ocF^NlX2nI=GolVr<5d?8TKZ;y1*UCd{fV-AREs;$} ziiUTe>bF3k*a&|b2rs#?b1{L)M~X|X(UF{&^Ag6z6q2v#-hNWvB!n2aGe(xd&%|gO zec+X?%Vcf9h{iyiKy@qX1iopxY{5sNt^9aR#j_2__Mc{l($NqStP~2hhm_xypN;FU*q{IJG>k*JWtQ=>S)|9 z-6k#KemQ9686Vt1YeExy8qaO?s>wUhjsNYS@O0--M-5{p&0V*M@K3E|&}IC1VHcb4 znGwu$T%>vvEC-tlE$;SMIXZB1l`e@H9VT$VAtm|A4RE_>z5Rw8fR5A;Fk*4YXCSk5 zAmL|5E9L}~-p?xvhdp^N^L#N>%oRm5YK;J8!D~hZ%m1|rkbG^eAU?bAa);b}Y#ChGeKc2QXtcBJ1 zNOc23W&@cILQ<8vL2r3my+MP*#y=&{O&EU-*yihn)XSn4qD?=iv2Vz0kdJ#CjR76$1F0NkhSdJkX+YZ<}kT~uGms#zbTPG${7b)-L z*qCJPeb#>lj338<<1Wbi$|nHhPp6CfCB9x~Oc8Zhd$q3Yul)_k*JjA*xK<;-9N#)( ztSnm)$CNh?XXJkh65Av)H#ZL|QK9j0&g{Ci7ta*^;&K6mH=S~Src_r6N{%(k(o$fm zfMY2u-QWT4azT7rZf`E%u&Hh!Pvsn!f5rurX2%Q$GMZROSXUGUX}bO{SZjQnh)QB8 z9Z~ykMC%6dGv(rs_q|Sq&D6-BxZ3H-1~ME2>yICanhPh@r6hw}YBy6q=a8 zE*pI&s*Z|w+K>(S-y(3M3#JN!kR>HbC;M{l2+4qWev2uo2f~HU)Ike@%JeljAjLzg zkofqkp64$$dc9g@H=-|H&q0!R&68xNM9JO+ry5>`8vJnK&;gR25}y zAZ42!OS0}|tesMK;ZTeF=@e}}e$K472{4M+-y>!X@&aA)#J1Xk1__l0 zP9uaXBCK|)ZhH%ToGySip`eZAj~GQ*te|M|3ep`yQu+{c{?1C0h6V161FaGqPVl@qsQ~^1?TI#= zSIhv%gDm05|DQTDkPrH_Gd1iHhBMXo1Jql2nDD(YWecQWNlA}9V$Mfbr@&iDJbbg* z+QJno1|%qOfn>!qP%&m#-c<(EC)LD+l0acob>r3DU(sL%Xg7B*Vw8!*s8D8?9S!Nu zJb|NL3{E|kwoLddx{17r1UP8!ZDCe#~O(_nVE)e zmJlCkZW+%c$S4*KvJ*FLCW^H>ckrpt@OVNszOu}->py}L5W6KTsF=1c3#S_!W`5ca z-qI67AqQt8Bsehp5JO`Wp8guskssVl210E;W&%VzrFgezoGHqGb*Qa3q55mEjyd@W zTMna`a8i+p=}h&Ty;PzJ{!!xQvacv8r!BzwOwd#AC5aF-?nWel0xWNK_GWp2$ay(q z)g(yPt#fU7?CNmVjBE&?n+eaW{Ts8~$vmSk#<7A*=IyVJhwQr9P zjrty!G^{1s}3hJn~(%w_*$qZkF7AeEXCBFaonXmzwB>{Q>AQuNPZAaU9h6 zJ}8XZKWOdLwryFRlgd<1l^Ewqn>3N89bZbL$Wvuek|9jCK1jJ!btN0~0)b7c**qw% zW92e|$1E4cAC)e~74AElAgPyx$|Cg9lOB6K@JAdD3$lo2B4MvxcD$)f3L+niNzvw&wB)+8;iR zbUg!h7m@VCF6%LDRL;A`1U#YH0tbgKqY0Oxhl-D$$p^`jc1UJCIWOhm{8WeHD&7uZ z7CEe>K{>Mm^7&q#Ub@F3j&9i)#*)m-ykj8Vc6o(Ui+aUaJm-p@NqK1FHcdhSjbJ8b z*+$dM;(asnpspt{?Uni=&#*rZQhVL_RV&aki0dAGlS84CMW_>+v83}ojKaGORR}xv zoj~pS3^?w+;nyU&nvq24UWc5ePk{Z@hNZI2UK-*~WFxP{qChI8zp99KeSp)5PT>BK zc^LWkEP#+mcPPTmfXqs0ZnW-t2smm7`S7e6vb29zzkKKmO_;BlcwMAgSn!rK2;uNR z-yIy)mA}N>ZdxYV!#|V|BjQ7CWNEaTCWL(OBf}CO#@9C}wiWLv2NFc|FO)YcT~O*U zeL(!2LA1WV5xbBw1UJJc))Rhjd#>tg9d2bhHo9PAy++P#4wmP0nyjG1=S6D%*h+L1 zi)QNMhw0J`rpp#Q(Qxjr6vrBzXUcrgZ-{-*%Nc}to6!=k!0wf24Kwsfn?WhpNy8Xt z!$Kj%3_R;sCHScc1L$#iW*Z3iG8r3gEYDWD{v5~paR2FyQ7|fUkgl@^yPVs}1 z+z#Yr-Vs1m^)&q*u_sW~UEcji`+O23u=TzS8j0{Zv4G#n3q1xd_gF; zm^M>AuTeRPGt0e-=Jp6X!-C!oK0QtDeqWYjEsgK0q-f)$;R$ZFk}0CFMbElsT*qD9 z_&X5FW-Tl&s|IO4nn}u1QQ5U4!T`-`>WNJ#D<`Ttj6ZJ32!L&yWc1O0d}0V6>(*A? zA=;Aw!)Gc@Wr5eK%-#e+jEFED3@p?+d{4u2A&)b=tPi?C6Ufwp5;BtzG8IZ@6U4tpr$#H>{%0beY~$hIiMR0$IkW8SIgy`MqMt-RbWgJ$SJrm8v>jMmoJ zIAblKl79jpGE!=PAN1@2so{RperxTb*QH=&7p~Voo_GWcA`&VQ+gyu0YH}R$!-KT&&qQeE%*LazA1=bJTD@DwqeefAY{Iu zThmo}mJlNA?`rHcBGh!UkcSYQF7MmR#b(p}5)=$1iSP^mz6dhZnL8|HU!1rJ@->lA zIK*hq5KHyzx|Rhv39Cg6#(VvOl;E;cQavV`x1z9aMcF4-4kT2jh~gGTBP5FQq>IB| z>V3v{SP69WxbQCkFq57JYQS8W#uVl$qtE~_E(bYUPDYxEnIdBlj(JFtBoX=b{yB$< zCG`(g4)$L%H2pqV?IJ_%>6lT;k+3}?@2<3jUE$efQ>4I=GoX>+(xX|c{+Z}pt`3>KAEfpZfPB9G{hOmWXYgGQnqZdVGl!nukqrKj zIz9fF{5om@2mHp7ppv`4Sn`sWB_&ko@}(rGxDfV-v)%*owjkZ$8BdFC>k}&~;xNYO ztt%36aKqN?n6mE8-C*mA>7iZV4}O2FeNV#}31Hotyfm+w2I6u=>ZZ8BNKy`+jcFbE$! z2pbz3f`C*ifq#3z06jv5j*1GAw9+>^@YMkDv$*ovM9@4S9#_1RT1|~3+&F_Zdf(p2 z(@@!W5eTZ$6D}_SNzBD`8`vQf+H&txJluj72y`Tqi*-P`^1J6aB+i|twtrazXmy}2 z$!eNUJJ82!1M)dvh8NPv}sT165vDK7oVBm-zOw^F75N;BV0uc z_6JIiU|$C?mUf)=IBL4?%mTRjCe0I;z}7gkxv|&*G>lHn5n!y`ZSvL!UvS_IhU3Uu zmaM#7!0;&fD(y8uCbeJyU2mDDSGdD}YBMiw??N<_cfoBv#pZ3iWZQS4{_=gda5d~- z)n~B8&SWsTS$aLd2X^QHQjU?p^-lbD(26ap?5C$2m`}XZ1O{PL^kVNtA0E?LSAJzI zMHB>%{IMFbZk`WHJgU&i(6baV3?s#XSH#r*;_<7@G={p3uQ2dg{BQ$X2%pz2dNon% z#L2z`c{qRPE|H)`-pkd}YgKthrJMcRn?bn^e+FD~$4I!B!$14be(1!EIc60C26*IA z42SWyQ~6s0f9TS-w%iBb9pUa>`63b@ojD17pTbDL;*Ss_5dQE2i?di_{Pn%?s(YsU zQfeh=v+s4UeAg77U4!tyi%k-0t=#|rn7Zn)D7UZ6z{m_zLw9#bmvju>At9ZDAe~YJ zLwAQr8-R$QprEw0NJw`p7A+wl-Qz>9+>$K+J}DS7GH)7jfH=TWfK7itn?>{ z*3+PSQ?XpBy!p{aMW;1Z&LD<|)p)&ZFWtmsCuGh2b~8Pkb3ec1qSsk*Ask>5P7f{E z0gdA)<`VH+>&oT&vH9T;z`oMiW)(3^b?G%R9rU7ZfZ$^fKHnLD!TLSDQ5k%%r3QkQ z4QX=JAJbop49-!qK-hK1{1SeIX_5datZiUAS>oaG!@>cryHC0F9&R5KI(&LQyIzI} zQFkum_AzKRljA*U4Hg&B(Ze)YhbhuEmoOd6$`n=>B5EhmSt7q<3610?;^qgdB8{Ml z!vzIwNBtt{fV(4qtZ0^s6Bv51ZHA2#q8-jHDOq=`2!<++(h}r6dG%uO><*qz*prn2 zioQq|N3T&m5XjP;f86{QL^qi9n1?+1X2W)UUv!s;aIaGNLE4a_;d)uH7ZR+`{#~o) zhdb@(o{~3}hx`Q_G!oFjiD(`~7Ar>yyG~R$$q5_t+_ZNu0~6qTplcIaZ0U(cP?+qj=7 z!{42{RF}*{biFXh1UHEk)*Jev(swl3Y@_@0=F--TZDhD4$x4&^Ob(5jGChJe9Zsq{ zmnv0iUg!o2ojhUJ?|_?Csl8!@-la^OIs$D|NBLK2n@!#fQokmRi@hSklBlkG(4GMQ zpozjdn~*JvH@J>~PrMUGiH%EEJP0b^9cfX1adWrXR`eX#w0HfOOi}BrSSkf>F8>=_ z_9Wyi#60gY2qLB=qK&mU#3=0}iDsPb5oh&?knOQ@HG+`EPc~uuy{)%L_bz)W@Rj(#JFHT`kr)A*yy9f@{b#n9HJQn@$ycuUcF`JlrqQYJ z#4JBR-+H_2O=16$SVradebQfvuB`604RMz5dV~`LlQ&y7mN<-Xo!LpMzr~R0)noFj znrF}Gd{|!gVfs3GmYxl^7wc4>#n*q?MG5R==y2BC`GRJyR}a_1mxf79nbsOAfuY~* z+^J(Zyl&{Vm>7J_TU}ft;+*9EBG`7F44a3WJhJ{PYE*X@*e7J*9&Ru~~*U8Z}1`ghBfT6Dce)g>HEw6JX8w(NqY!Hn+qLx0c&yLgx zu387of5zZd$P6>UaHs64nySv87Kz*M4dSa;_9-`mj`3S1ws8hHdY~)=LY~+c1=$2! z`6q4&2HyKvBDc`i%rEUoVlUL_3UvMu5^|V13SO>ONvT7SMkdZUH<|l=pMN}aJW7Q6 z^Lx|@XtDhOY1dV5%6!9(;7Wh~JzP>D2xo{LPTV!!g2kE=dFlsM9c zK2Iv59_Mv^!~;8PaDh=F*5ltt$x=@;KRI0;>FX>$N8+f*t1{SY^v4+73Gy9MWEGG` zpV_f0>_FB$XspJLF}82krEi7l^te8@^PN^3t5xKaIMCoz{<1?Us?R?&6MQp}8+CnD z5dRk_${v2GG=KSJ&q@dtMTd^5VlQa`&DmG13vL~Z0yc=#P!*X@88y1D^O03D61@do zy4ZRbteu%0VITF-E*Y(Ndh3N>sFzF2lHI@>hRa_lX0~Iy%ZFaym&Y}(4fPfQ-AKYeTtdun=m@H ztG9q;A??2Mq{#DbEeYQ1-Xq;wGkYDicY5cv9{q&`Q6!f)NAX8t{o_Fsj6EmiaK3Nv zH%Sxa6WgP|8|6DC4;v?OND)h=g?i@T?Y)|KT5_@ANh?(gN3$$>NwJT0u z-XaO~d;Xjche2(l!#W3!X=Q8u{}5cKk?rFWbgFRJ8h9O{dsBW2tGJ6XRTO=7s=6pf zJ3REs9wQ6T!}>%c?Ct2n%8gyn&>5Z>Wh>kZF~zo_rR-t53os`+$@;Qaj`hQx>2WzZ z_pEQme;3aFNFd9g@2ys3LACy3Vs1-?er#=wW$g6*LVl5NeVPm4><5Ho3ciYAkD)0h z!NJ|hPrvyw$Uh~lf8R>fJmOTyZk0hzYGY>mn*6TlN%08E;Cb^+yMRN=WK4rvxT^wL zNs)52RVpsc&`s~xUaC}S_{9rK!9Y{dIZA63#7}fl>V6m+PsCuwaQW(*<0%N)J#>cIH0c9u^XI)M~4O6_bzh;jlw-!e%YnvPkI?h0B!-#<`(~&Nq%7hI4Ak7eQLh0ABoF2-+kH6J*f53@3@o<)V{G>d>A>&T! zZt#-=F;-^~WD2DQw%SRDsC~MT6r;q3VFmS0tO-CJo&RpDQ^8VpVo{RQj^>128>CA9 z%d)5fxmhQ!;U4Wo5xvH-9ROhnT{N9yz4>Is>w|K8FcR0rEn3rs#CbC+e=hZSh`9)o zw~LHUZBdA)ZWF`m>RpX}O<(`6td!z#3r?i$Rz(f{gmSVp!z~e0vl5Ij(6aIOhLc&D z0SAMNKZFE*8?a}?r}b42Y3?IYRCdib$E!ri^!uqwTaytLGzxxJ^%`Oqi~2gP!}PEC z9EHwsOXg_hyKrY`=tvkN>mud7HC3dby5GCf(r9?B9!2As@y0+L*n$N{Db{n-GJSj1 zsk9}eD#(-&!dT^^O0v+UXZ0Cl4gJZPT5e3+ry+rBRIy9hZeGFid`-oA;)+iRKZh&9 zcVB)GNM$tqZHqhIx+1$V-2911ROs<8KGDvFqPTo*KzT1@>q#k|Ufo!`Q6Hvz>shaI zbN}hc>pH-;OKVXU@f#7}fTawtJ2nl(ODTHT1ynLz*K_F1lOVz;!ttGVLelZfNTm1~ zH}!s<_p#jt7er}%D9J^G2DETLOvv!P6S*Uff;$7h{_8xn)y^|2VV#I;T)%lArxHz* zt@XDl9yzL}H%i$(AuR33^aLyxgvi==k$etzL;UVSXTNZVokZ$u8go2Qj1RAY2x-A+ zrCpHd@T4$5udf6qkxAUzVN>3#RH1b9S9gkTrPi6ZU^m5p*GQI|bvSxNG2eaFok{1+ zCL?8mP?RMRcO-G>M=$=Uq^L!rm18tEr&Q>s46N>hG#Rc_*XFKe1rhVDwh2O!Fmtrcc&S5Lia)RQ8*yANa%IsSP~2D{4ASAGg1QQ{CEbd%V^+fT30KTg-*?Mvn= z*m}iuknKWHvcaWCx>Oq0WM4h+P}^2LzaMOvQ+IiS4L_+om6*a$^XOH2eUf`;Hb47I zrIA%aR@=rh1^Fl;Cm#pVr=~c*j(jN%B3_I4+e-r8d;>?lhjoI>K{-ksqVojZWESZM zbKK{&E+X6~IQMotlF#U=vOT^%@O;ga?dx=U`u4pMp%a-kmut74OACBt8YYwAU#w!% z0|6m}Qa~>e8vA8^uJ_zso1Q%qn}g_Ohzx$DM-4<;6krMA{-%^eWtJ?D`yUhMYey46 zZ@7k0(+|y-!p$5@UbVlXf%TR)v!F=F7($7N40;pCz6W;r(IQy>&OZCd^IBe73JP(H5DcvFbHkr8MCq zPq*dKqZD?hzokI!RrNEk&8)%-d=8@ZOO@;;e1*>bc!gX`RcLOehTqVPDKXElRMLkL4gDZ7QFe7S9e8D!t6; zFn!{ldoUG8VNAbg#a=}J;TFA9s4WK(ZO0nNmg%EwgN0(FYfRa-_DAhC{EA8GA~~AX z;Tj^8ug?Hf0hh31D^2wk0NrE?L(>ai%$p8hh+qF{nxmJ}vyyH1v|zCIe!VE4s!zsn z3Pae)`V8@aZ^ni0bo8dq?Ur?h?7OHP>zHWws+T-8?s?Qdd>D|?>Bv6mHlZ7^h*h0N zkzG@86QO_F2`?svl~P)9Wqz}P_9_|A*E=Mmomjced@_E9=SsO=nB-1IJH?88d=f&m zJL`nwKb=%xADf>STeNQ|6qabw*dtBE#KP6iiA=H!Op5K;C-LYeg`L_uL>w0LxI4al z4&HlPgY1UJ$eXRsS4rtF$v0Dr5Xl*dVo{a_wh06GGmyyv=BL>v(qj{n^sh`vHOb$1Z(+lfhV#DNfh%?{=iQ106Yui;iK9bT&xb_oq#`OVJ6W zinB%%Yj$Lmfu$}u4Bg7O*|y|bJ%y{?RVE={v;>#R7Ksz)40x#5m>1suzBY)oV3s%n zfX*jZCEK^YPL>Kx0sZ}4=_{k1FHY<(p~(biMN(H!ivu4N;pwp3U4+}ajKo(GjtuOD z=1{R4%QJ}pwc_hOxviT7oRJ-FifEETDYbRGsiy`-WzOd=lwX27#&T6#_T4+f*hQzO z8(fQAUH0k1Npn!g`EYk+35!3Mq3b%1+G%WgSC>moYmw2pKYmuYMfCVCFnc0{gXI$0 z&(Zf;t=y?M)2X;5Sfz**JQ$CoP^M>3HvMu=*u8u}-%zmh)avgLV!qyKB{r6|0KK=` z871K|OkHVj|30&xjMK=4mAaGklbC#iF)Oh|#GpsXEuO*A>mj1t=SpMs``w~rNU<@j zj6HRZ1ALj!F9)yNj})ABs^`uQwbsVOZteooW*_2r??;c$?rt^-z^59Lk@vs*k~{7KG|bwd~de)Xs@MAX%!j6D!L2Oe@-xQ%78l zL#Qs*@=J_sLTUNE!d5arg#RknDQq2fv37cWA~w{2STdn^$oTSghIdD`5&QAYS-?VE zS`}mxUOx2kgF6omUnjgPG##-wA5jeLbQ9ubxkiVLi%a1DRqSGVBW#p6c*i*`sg!1a z#zA-inBS~QSg|y?tN{o<5Iwe<_xbC=NUQ=n(BV=9rD#{b!Idhn94{@Th+G}BfIes5 z%+e!G1*dpHN#a7s3{< zm|4fX;+HI9vu)XG%dhNS!^z~&amThlC3cOf3n?MT=Y)fq#aMBOI>u?feDYZxL8=eS zG5b<?JY^Wfg?>Wp0v%BplwHPNt6`NmY1N^5|5=hbbW`Hi2V3X$9}W z9QRJ-$N zptUbpHboQf;4>Z(?}t8dx~>14d0z1MyVIUHC!N(E%Nakn_p`HU$GH+@k`}e+Hb-NW zqJ!$z%`X}Y3AV#!^kqg4l`RTFp2(>?{f@|pc+j4##I<8C>vsgvxj!x-Qqnyo!2nM5IgjDr3=rFc8`eH868n0_}T6_B!S_gA4}uNUU8x&NK-{>qjB!o z+-F3(m?t88%9yCwIx1^NlcTtCOr*)uHoRy9gEu}TY064z=V~q&PO}Bm&p6j>hWJob z@QeA1Y4|Gw-?YN%sW0m2K0jqP?Qk08r*YxEUSHH$h4E9Bp?=R)u_R;^tHL_UPtf&< zX8X4_dZSPn-Gi0iC0fklY#)hh4&BxCo>mAZlooSoEeJmt)!Yl!nV&2qni7;hS@9DP zr8}MaGzTSPAo@X_@se3Y-sh^o=aAvnMT^qO^&OmIq8(%JwU!MVL{_%77E9zGyjY=6oizDhQj_>NcW@yZ|J$+1p zwhP7R6T-t+fA$yMk}dr5sy-h5l%jO}or3%#qJ(H9?@E@>X`nad{w@AHoU=Px#^)(7 z@r=$%o1iu)$g!#ms!H!BiSx~TX`cNue&@a*_KxJGA14p#dS{W0K|X=hd%bqq;o310 zPVJtZUr9{ad`Xj|DRI^?Oh*NDOgOzkUNz zj6845`DT>n+#8+NTY_BB#Y)sk-{fWemkVGF0AUyYVftZYaJKam3ho}CcG+fnJ5KG~ z0*-wb1=b%zmP0zZe`$w+X4wkrUE1TTku>BvP{W6u<5*%n~mn}H<4$#Wq_TJO(AwTB&q zf#;)maSqgU&9QnBC%rd~GfhAs@A&$jnh4|5fV z05OOAL7@Dpwm3=V^&kVUa#I=W8ep81h7B~?+>5cLLx>fdliigST_djBqFI{N01@Nn zUgJ-EKh*g!Ri|4o=P97U)ydAWOEY$ig;HHy@se~o zZGbwl;^5IU%L+ozPsNqHs#u_jMpI!~Uq$$4)K`1o6bjy|pd~p8kq?I{13O07c|)#15_5)fI`Z=0wryl z`9ZZc^R{nyFvtB5XavghbJLMEpgm>>^W#67QIAP-G)xGvvNShau$J>61L^>!*ccBn z9-Ww6c8-wQBDF0ZE6vTDJ;5&D10Fc;B2PbgwgW3|5-v>hl1vK;DGhtKp{_<>@5dBz z_U^|^?aIEJwdcJqjG)9d)6j`O)8MDJ_v4Xo=F5~=4t9LAKLdD2?#&{9x#lXC!&eV9 zq>MP_)KY1uToF2<>QY~|^cyDRl7WipBTaA$;_UWzAOe_`f$G&Tl0FEhnl+#b~GvTOVy01wc{iA&4I5S-*T|3KQLDId;k8fJI9kHYh03U)K5ApT- z2{l-p!ECN~_s7Zstx5m!5fmd)Co^rFk-xU!b{y`i<&<&sc#H%DpDis+qdVN}nmJ|H9CX6H&4+m68zHC4kHCtfN37h5O%)+=00o~ZE z^TavZzuNLyBpOpTuR5uPQQSatE~gwt&EvDzukdI59zDhYX8z_E;1`u)_vD9u=Mxv; zQuBhOt;*bOxIox*I(>8?la0W731#Un0G)u^7{Q)3VA%2;_AAdZPG?10eGXBl_gZHf1iu)NfZ5s+Cx>n{3m zUjZ~Dt<5Lu$w=aWG|C=OF>02;;EQtdc)fo7YZ3P&?XROt>#44r-<0;>MrhIGR)d|q z562i-A7i{s3=vi)I(R@a__Tm|OgegfiL4fme0<0x(SLxMYmkx{1Eh2#Eu*|Z^k9Mu zHbDT8=lhY-$_Ce;?}e7nkT|Kx8m-%nJiIwwZOr9ZzS!)u@v>T7DGehZxd-|-eVykR zS(panoE5obRG9%>AnW~Q!dwmLD7PGlXjD~K4}L!QtH~Y>-L~@f+TwIrTVx$nS_TE;L<+)CRJ zt9QWcgo8YVJ z1Yd=(E76G%KvQc@)#689mDI$ruHu3B2+6;Ebi)C3`5+uP0>0~5`4t#t2w`yLD%W>5 z?gl>V954QJPP-+$e0@RcgqLe%-KZ!i{%3E=*|5WpQ+T<Qb*qcLD6t;e3w^J?g&%=DG4 zN+^1ym86J`4NWV5{<&E9FV2`!XME5b6Jx+!!-!q(JoJfM0uEt;K*$m^k_%_j2NV#e zp=?3~t2;CGKV-W5U?vP$oql}W2#hFfKmoF>E(paq_nd@=6>QqH-M=Z^8Q;a?PzIO; zXM!np)HE0t!##FN(w2G$>pkVJs~-!dDG=!!6FTq!5 zg=(_Zo%L&|D;fgM&RgbOG2)$(1#X`{$A3M`J0}5D=pcuv!1>4oU(!-|(7^B`uNP<5 z0-9TY{Hr3)I6F}anTUkZc$8pp`k2jOc2n`zMZ2mVy|>D2a4m3%RimG+2kfyh2cSHx z4gj20(yc*BVoFI?(&%Ui2}x-%-ny#F%?|3b&$ZukxghgZ!7K=-X^~7s8G5{gW9v+Z zyuvi5iZJO~gg!fUK@Z+_TviSWzl}%Alh!ljQDG$Tx7R|HIq;X&rV18ua7 zZ7+A?ccc;hD%I%v4Nn{L?)`?t$4pSb1&xh(xraW^19Z`KDpe!g-8>|W{HjY%Sra-G zh2?Mtp`N`+!()(docVLY9jl+h z=$uzDLhRdvShsO_DNM@I{Ow`Hr1) zK17mTdFdbo$kdj&GJi?ZaB%uYJFwWuat~QME7%%7y9C~Tq(pz*UYtEj;s@t$v;eWU zqtyI`=eZV!#SC~If$w@Q*Qq+{fl841=sZs2CJ;8W8JCPj)$(Jx`~-%+Vr!ke&3FbC z2Mz%m9E|YZ8g{_>>7+#=MK%9K0y>0Fp)L?HIHI`}64tqrkz2Gm=`xSpowQL+4xmXU zCjD~9oPPCTXX%m0b241k4tqITvX5kJfksLKTCk))ZF6{K zH*k!c>Afa9t9QQQL!7U!@;Qh#YaM0awTP5Y96l~I6I4)7qY>?^{#cd6rO?Ft}i2ED&`7jcckpGbnm9Nug!?3~xErz~M!7gwJM|M)zE2XaPyp1dM{ z1%`;k=%P}U(9x)Evo2(_B^U(5Y;0BzxI|*U`N>qog-0Fx_k1fl+}?@aFrIyw8)*+0 z;)K5*9gNVIY#6vdi9|2d+CvtrX;W{-MG!6mr4Z%w$*_wdzcGY~qP3{CWW zi;0ltH%^S#!Pij9(EP$K`9vo%E|UJc`VgbXXONv5k>l@E;3=>MCQHfs9vYVF7c|Su-n_4#cS=jVvcYDP zbDOuA|LT7)uwr-r{3+82XNA*VJMO}d(_>5Y)zL{EwK~$2o|ymv=f`#F->fIASOv7& zWF4SJAj!QzqlZjhQ6*N8ZyLc?@UF?Q;Z~pC#JvW%PB&_SJEWUJA!C0NDZ_{1k}Dxm zvJ08gl?I$3PMkk+adB4l64LQ$a%Aoq5>a`pkA3n`5#wD7UYvdm3*@^h`51j6~R~$=Ngcc7r@Y4%A(fwRF2f9*;yU(%-|7@IC z55~AxILeW~X3j#<%>>w2H&?3fJgB8`vuN)=fbptb))== zrG<6Rp1Ds3lRoFnj`EVW9q@I`(+=PE*my2bIPx1rFoNtXcn%)~w_{lsjt`lRTHU<1 z3#_piHhVfTHk)MTs;DdxFkgHFwx=0?iLj zQY*+;=P?e07-s8FkQ0}$sIMc*J{p1SqT<3a2pLH4qTZK z7UnZfv8X5uV9lkds9xcqJ>nAi@~pC|Hb}YYV+t0Tm6B5DZWnUJ7kH&f=a&PPwsW8% zLv9Y1fKJzI>A4>_?*+UJlpenly|k@3F3qSPtR-(}@VYDWF#uzVSEewvTC&@uXeDUC zi=9acV^d412cpKgmP0Er+o^nIFk4T0&=ZuQ=kJ;R*kMRW7*TXL8_LVuo5fvz(XIPg zD#m|zKoU~1cV2HmMzN`oi%wr=u(12Qw8TCRb{RX2uiz8;*SxjCA zIlfQ`iRLK2FR4}s>m(3{+eoc z3y!=^Z`b|4DPhkIWQ)pO-A&HFt;WV2|2R(<6ij$TKQak{rYZaheg_DgR=snHCisqg zyEF!=pj0!*X=Mg^fh2*ulF&V%eQWTpoJVPn0Xpu1^ zzd&}}1PYE1o^QW$zW=FN{Ss~k1biG|lg9F>zpu8rWgX*Fk}q*1X7sZ>i9 zvx0|ch>OgGc}gmBpbgmNMx%^~~(eP%0&l5xc|2fik-yHv1|rb{!QA72}(U z;RPLipV5yar28x-*_^BZ2E2hwQU8b-`f&vsarw)vYeI>d*R&0A2LIRynWE{!)}bWis1Y?&1J+OkSYt&B<~?JJl49{*?SyC}I%TkS%a&GIJU=~3G^MIB zdaU)(U`<35EQ?|f**aFL7UFhHU7~irKrZG4jkl>Jkp1yy=j7D7d$*?2Z*6s)W9+%5 zBnDU}isnkZhEJKYgKCw=%TSzzm$ARz2hT@w76qzo+ktyrJhDtN3fr&iB^UsZAO#FoDndQVF_M$au{D~9e-%K4gHauX!^qDs_Q!2%`iIpH*{x;5+rqMAPTDZ%UgH>HYOgmQ@-b6m?|J?@E7mEe+{=+lwhiPtdr8 zS%A$aEys|?WRDG&C903=JqD5h#4_s`_0=t~4S*{JP?njB2zNsa?@xMC@S4;Qr2>~J zVOrx=am;#jPC#YZh|(D4YRXj;$W((I{CNUDjTI2;j@7k%rZvkk56FsL>3eO)sq95~ zG+PFCc5^<~kQJNNKrjsP+l^m6js6>h(3Tpwa=$Ofdb|9!F|e^qNMLrcTrI&B$y~pJ z9d;WF&+6}HPRNk7?E6$H7Et%t0{;joBP%Ul2I?KWtfy&^0TqO`rVM8hF$F^{p9@Tu ztH9i*YYX3Tj?QIZf&c3RUa%Iu&O<3f44Y9Y^=mGlnRHs<@1w7@Pm_F~AmC!VWSvnC zz~!h`XY=y;=JhZ&tZs&AtP;6|sDSU&*J=@wRI4nVDs;gCZkSh8#9kxW15*46)}Dk4 zB|>Q~HlA6xXC~EIVSpqz&mJF{@o+gcnQp~|vs->0tNShoReyFVXH+r=>eCbgiui$m z!uiDGmT?d`PZnM4tXc#Dr6Nk8m~*EnYcw?3ZVcz46>&;f1m+7OK={-z=>vF$2g9PH zLc(+V#m&ult0moS0YD4?OvujqFnME8KU+}>3TSzC4t8@do#!o`0B0GJue3}9g$$)G z1F72@LDwi|C9v?Elsb(|aB8;zL7UX1NGkGA>)YJoh}-FJ=QK4nrHR>l$6M7qZ3^Lp zC*FIQ57rac31UanB6kc{D^8Kbz3bG?z_!M;#V`Q}tUq|9qCZ}+9z7O}cCr-+vPJAV zv1ov_+8^(lPXK$HZi%eLAyG!8;vo^lM6zoQr*8(zI|dkJcoHD*W1V3&}?Kpi$EH31Z<+Y`Tm>ycPs&nKF}BRe~Xmz4{M zgH;W0Jt$XEe3!Kh&B=*E`b*+B%OWf zoP7L9_^}#Gcj{UcQN$H9bT#D0tr~K-tiKkwIR=t~51Cgje6S?NNwa}>aP^=^)7>fG=u+{|55(OYy9tSkbaVp!`Cig5ns91qXp9XZ z!3G~W5Dmk@%jk@S5-~_WT2oddC_AcJ0;ZPsxpd+!a+?}|t?{ZSb_oTnbG{=oG6dM5 zZUN$YCNRgnsrMG-dEa`$DsNb3u0auTaqQ~Ud}J*_rBb3Nq>88Ptw6BzW8ux{0}o(i zePb7^CITY_3ffhc62)9m%u!%l+@>!(qkfPmwD#k&K^%=0+3;FxoohG2Z^mc*z?MPFOC?Z(oMM&Y&r>#`c@n`jkND(g7`aD)g<$&STJ6- zdinN0drbi^*oNND^6qpk51&~b`H@01fTQS_s)5v}<^;P95HW znUzm0zDqAp2I>{mKoj0U%o&7Lt`aEP2GhBM+`02%920j@PzTjcs&%`g?eR~5L@5Ua z)fuPh4-tA;2~IGAG2+y|YteOtL=pBAfK-}0i!HyHW9TVp@p|~8z{dOlAP0V>Js@2$ z=N=2~1gq>0_R~5A*^Dk`Px$D9M`P_b1m8|q2=nB2zkVW1RGx1e(NfmPM0X)|DC6=% z=Lq-(Xo6_)un?}!0fFI_4U4qmaF(9}>!oQAtsHK9yl5av(JEJtrGLm1&k6Pegwz`c zVw-@2qL=osk_FV=`5QFx%1v0ml80*v&d^wMmX zA(!<*`jSNoe)F-xuRT+dKXBznnd@Y=(I0tx~>@Y8mbxtaENY+JR!ZYPBSSSlN&zwo=h zl~x=?@p`l|5U6tOb&AQ*47t!iuBT*Fj+J-}560KNa+79+a3>|-OHWn>4IQzvN$%g( zvxHmsS#X)J8_XB}{ZMc(A@*{mu&|6#o03c+pEkrwny9fM6b9eb>EeJRTVq90M*Kq5Y?E< zke!{4165L1j;6{VxS9QTq%1(~_CU;mH`v0bw$PyGrsxODdh-A=X&UBL#>#+2g~4w- z;gyNtEA!gb>X2K!ZoPek*GXr`i?2lPG2b{ydj#l#_y`=(XG;AOBZd9tR4nFWWy6-1Cmf75}` z1Y{1{2#mp|FTj1(?~u0g{`E{OyhkdXZ`SR8%r<-iS>4Zr7T+zc!Mqzqj*^{Ve?HdA z<$ZZ)lo2T2)5JU%XB%2h@7Wm_R)Od#68_u2<3{@TeGUrqf3ToR z;BSUC`U=GUb?bL(L&Nxp>hXZiYCK6C?3%aWV80&^S3*mO-E{}%4;UZ6@s?BPy^&%W z_xCa;Xk`xxKC(0CJC_YaM>Y+iFc6kJZV0fV+f(kINy8ED}!2fMXgW1t4NHE%#p(xo52GYv=pn35K=6q3Bb7iRK(ul1kc2>1fnhC0no`|1Co(UK=mCS zw>|?>G!D=&&MTWITL0o^LDmckpG{zmFeXIE&de5uR>RI@uaCTn;47piCI_k89qV?3 za^>5X!|>=wj~;0deRKy|9cHyr?(m*eDMc%x64^=%N@O>%tJ$69WIiba#e?tBwjtrI z&M0)AFeD}#*c&2OutxFu$o4%G$uTLh#Tkyy&7jZ2g}kI=^dEUtddcXKuRWJ|Wsjb8 z;B%9>{Z^_M$uthCIq6po&d}sk!|7g$yjr?#0#f@e;G)zxl$r-JqjUVv*Z(;otE}*z zVkt!~>zM1zEWzNW^j*03#v0n5e7?OsUfD!`r_!O8e)V52fLbb)gT(EM4JA|$s=}|5 zj3JvOG~##3_cIj7pb@8QuwRJbQUzNu@R?z+1sMD*9fDgs3j5l1Xt;HI=0B(ALXUZ; z2O@CBe?e%!S``B786siyv$d`jA-Wr3!Vm#!-~jH3BpRj2knQ9a!KHjV8Q*m)Th&7g~D(F?lH+wDVB><9cs0Y?C;L$G9(5d-dVJ zaU=k`ZbWZK1tQ5K{ck>dm{A^^z+WN2!%rt$B1bZOuM5Udq}~Vvr9o4kCq~*SLVp+Z zjt&5$;M*t|Z4g>*-r&&qwD)^x0v2fki9n1|<$z{`CHOU4Kv(gSH0kim=x%wgPtCsv zl?dC1j|kNP&d`1HpCH={19oSHJD?Y-E+RK+GaSgu`4-?6x8F0Rjw5i_Bvn3%-`C0E zw=hB+8&(@V#?O-iMv-5qpBF`xYR*Tz!|AppZiOH~qSn33twlnIu2*kHXpvWTBmUc9 zir7@@%yqqx?j%}CPkz^k7+y|byQ`lM`l*@B$vEzfwZLdqIv%{i5a^*&KDVcT=L!6E zoPZ0uw7e_~fxX90Z3)brrgkBN{Tj+odbu#k)-!6wF0A0wA93sfaBP!?Xl8EfxBg+u z>otGpaT?58(DmE&sIP$H&kfR3svIF8DrDNq(iUhiP(iG^y{V2(qy0Mm5=^Ob?`FR* zRBPU#d@<>bXOn-#+f7ZfT^8Xgb~XU_1sT6li{*S81Rjy3pOgW3Hu_!~O^ z{Gm=q=sK`Umk!VhWm?q)79aA}w^HojlHZozy;-$$Cs-3Ye!7&o>>e|@n)n+`YP!~N z5+NMQ_Nvi6S{WDH*(S(N*9t zbjM`>n1A#U386kH>j(1TZ>K`2G}EMN=ZR}wxMaXK+c{xtns{dVJPRa01G#24)h)2Z z#Kg?Ejg>P>5s<9pS0bjILrEELBHP_(1bz`IIz1&ER}p8)27d*$Nd9+b4I-H04?F|< z3xz`{sRwEJc2YIZ!uWAyxp)jgOS%Xte;J;(sX^r_p%ulg6bkWa|F}%_mk5ZqoDAC%W zLCRM})%786F9Ynz_(rHH-*vqF4KKA2!(f>F?`<~BxGU_>**Q6nqOq0+Sl}Q7Upo!E z`Ok*{#aIenPm`2l1SAHcY#S{eOt>nS@MNt%b`cHAkh?QZCU^HM3YT%$h8YrvG)A7Z zWMdsT8r&scjspJ2a}S(A4gB1@9PF!V=FXFUuTn+qWo86OjK<28Zc}j6pM!+GqD1Hn z=oMXmJpyK1g8*Tm9T8)=N@;teB$b^Qi@?8QvwvC$;60T#oMtK(_>WtU>wtTcHbRlW zq}qssk;LcYzw3>U31|js{=i4c-fI;ohglOIr$1parE8e0IzW%qZ?FI0_)zsmsouTQ z_>M`6!_))2MAbjxaG2<0HEZHF6cy}zz`ixEiJ9a_~h+hqTlaj%qeoOBt1mC9%m=G1HE$;7n5;R6egitu0h(bS12&V4=|fT z?QPJmfLHt3lS)Y)AvA1Z0mXvs8cP`t#rVr(_OO%JCQLAu1P9aKHlVKYtsTNaoDE8G_e#)h+kMv{Cl=?I2ObP-T1ggVtif$Av;}WnW?GTkn`E>J&O9RIc zR99Ww@om1yGJwSxfvRy^FV5_LzkH7Xl7#ku&1)`*O)BY_E0z56$$*S2!1v-}nx|fV zyVr@R*#5Z)jHaF&Rf2jPP7P1&=~FT2hzM7DbyiUt|2f;kmt*2X+Aiv-|+*sC(b41(>YB*%76Xz?v)L=lUa|8G7Y_O z;t;9?KAo{+?8N2D!S+|FEb6@pkg(o1mH(|Sb5t$dfZjD-m0Iq;|Jz;VQv%uw z>sl72$yR2y;f|lM2>J`mrH!cq0kUNh+XZ5)V4&;e1#sKQs}3H>7c4!d>x3}bethOS zUIE#-Gl}^YBy*f?@METBngoa#$EyWEf?&!}$N>Fc-{!)fX@8L z(6Q$rC_FQ_KIG%`zF(#IvBspo`ZM0+Fose8$ItkyMrADh3pRU;L>T?=2FiD4I9G(6 zg@q)9$&F-f;S9dE%Aj}!c~{<8<)#+*0s;N+M3W|v zgS~;!WPOCg+7(Pv_x2mF5}w9nvbwVC{k-wD6*xA1MVl)EriN8s@d?-x^MQn8qW)>I zOppsVBQt`T9D~c;sxcvnc^}+c{~6@`cU_73jWoZ4l;z#6yW}Czz-QpxpyIJo{>?)% zz$78K4wQ_R8r`x%%2ovzT88voE)d3t(TA~60-r^-E;2tj)(gfzXk?|A%62Y^%x6C} z*;?BUo5Ho+*Iy}@lb^03@ImQ%s{xfy_w1`(D#-8g5OP!#yVw-5IwJc3YT#eQ!^49N z_HbKUf)0VfYxFM1zXz8Yc+!Bah5G29*=uY?@5(~-iFW z``=fa%_Sj#_iMR>_KR62sx=mjPSXZ(9fN9-VaS3(_NQ?_tZ2KT93IGbr7!NG0W1j| z|L?f;JizLWN|kc>KCXfVlM0jb`<(J?LN?@J+nz%I&3^QqG7z+BP0Wek=m3XM9MmsVROCV?^3R3+$5bZP z|5nizkznZ~6trXYL8acYpD6i8kK)7=-CwuBF?SXEBCf)Y8mj{*@;ko%*VJ2eR&xiO zimj?c8GH~!EFVM)*fp9Z!RvSw(1OY(t$YCngh0&0|8f)1>c~~JN<@Qdex4Mnfh%qb z;*mC@-Y3V>YwBvmeK3n7&%peRXR)|N&XJhCI+gJyyj%g!_VR=bo(r!LnmdGLMtchFcw2 zH@32~_|DdlL;mvmkV6-1YF-8<_OeA+mpCETV0?obF)R0}n%t>#=0*2CzkjYjOnEH^ za3VAa8&%Am?m)&c^aiBN_cf5w!XK}oDcndnDZ#C%`m5l_;T7px#-|acOC_%=0>SJh zi8bqw*r*2U+9m=~(yi6uHZ2DF|}YQZy7D z-*%y4AAoTID=w$eQWl#8i%r76l@SW6%)T1pgW_}ogvtpjf?}ss?kZ3YCrF(JDv%Z^ zM7Afz0Ho~eK@#N+-kZLg8P@LtRsCCBW2iJ6NX7^E9sb`7K{3cP{V~AXnmAcll)zlI zXC=`+Jw42MBX4@>ApXvM5JuZd`|?2Tv_AS^-Ty5yX)`sZkAc8JT>Gr)4TAkG>arcV zRHz6mYD{#Knt9O`kf*R;kkuj+Ngw#xd6`oR^bWt}h!53`Nc-=10KnI8y)y=}W(O0- z0vk|~Ey@6o03g(WG|2+Q^W-sQzjjw_!B{xG?;_g)88#j0qZ(SJU`4y5Z&TU;ZfE#r zavJg9OL<>|j3%JLJTYy+m~sMnmMPp2unvz_#7NG zhlm*$@HqsQ9s-Ad4G@qvA}Zm`$53WHT|YS!)}c~`$RAu?B>(*><~XPo7ZJv~7Ifj$ zSc8G`zzM)%!aZH(8q673vZi44NakeCEp0!K*JlQW)q|WV{XdJ_O$BF&Nr; z05JBm!nO@}73UHFFAibaN*Ky)-@J@H*fH6lBE;{TeyZ%*e&%BF*1y`eP^P_Zqd(pK#uf03|Byt%=P$oE+jE=# z0!+T)#RAqv7D_Yjv*7XvzeeKTJo~S2Q`sQ84jJpqaey-&5Jvo8bpatwz=g~tpaC>& z@NVJ{pgm1I9RLiW)V}*6x^z*t5ofAoZ7hg~2w1^&D7Gv$CePF4)Bh77(cW+3RnRb?cR;sjT*r5kQIy=(a||6LH`4NVQNQyc(XG7>YzBO zygk5(+Dt%Oo6?=wJq1|xDuM051?W3g!@Ww6`kzR_e^P>RfGPK-hJZ2cZ1Rt}SMh<6 z_M-m|rTsw=rRO%ABSur(yuXRj>tlH!2}?Z3bA>2|Aqat8p`Bd5VE z-}9euw7DJQhXkHWRhd42U`niPF#+H_x5E~|`OyZM6^G?(4=OA9oTdlFV9c!|#&?47 zWA{8J$HKU=e3o4e^^9b_0v4{CU~FlCZP!1I-oMJH2v+dhJ11r^T$Fz1w)by}?DYTc z*zXX?!4Q%F&YJkYuC6>D%C!v-Gt3~oMk)?v?AcPt=$IN~iHe-EXBjG?VnmrJjY+no zv6affP}#B+PG!jq%GSa`N~f}=LYX>>O8M@$a}Iu=pMQC`=ed{bx}N*m^9m5CRuEDZ z@244>Fl`)K8UT!}x!myM>sAM@SUIp%ZY3|X%#Q9T88U;ps~5ZJ< z-5Z6;vyD0e*uMeEB3lyrtuU6@2m~#ouMT0ngzx7=($NltrG6y?H5bu<(J=MS{_7Hk zxMgy>f^?-uZBtV>5K`&G6a0brT!Z*f^1Aii5GE}a?+>q~e}!bW^rFIT<`ipq2(vx4 z>+ol=WJ^VL+r1u!vUq|-V|SEdPRlG=vl~?}i+an{Ze$ARZ7Hg8 zpX8@j@djmc)-F5vh!3m1_nD80+6!7;E_~ZLrJLRZgi1S17na@O%bnj4hQ+@ zZdYmSc(ifDpNX2~Q>F%&uv|m`PUYj0ie!Tlz1NQx;xf+=r??z{FO((fikh)`O6uoh zHyjc{c2tGFwx`Vc9cjI}1`&hyKKwC5p}EXLEjKypy6;{6fq@t@PmL4DGAIIJnq6Nk zHmY#{?@%|4rf2TI{A$=Zgq4*FXF<9{2gY7H+s`L#WkuoS)7Z37*5&5_7{*q{W**nb z$JW?RSTXNkwoR)_wX2AAJ!W=%{dXm0B8QWN=tScE3YuXdp&@#le)cfQgajMu_BSiV z+=y;ys4s*f6Lt@$=OY+17QQmiQHOSnlD(t$>ADTM&n#j98P88>;jT#KbcIaANX4Ep zs|pkylMO#ZWwfw*Al^?#scN%;g;lny;~ZWZ|Mu zepSmKrO?*@6*$VdT3FWd!t07r5}nX*6%!HV{L4tg5AZeR9a z2euO*9++Sp-P=ogdS3(|5xKhCp)Zh_>T=CBrHuJ8l}x>>Zgg;W+z4ZG!$M)jdw{MB z*2#@{GI@I9>CXMLwY}SBDG|cC6B@+*4YMD&z7r$~R>g~|6{FU(EA<{h`V9C2P4_Dp zb9QzF(0Dj)Q|u+8cz^I9HA6KlZhb7G>e%#Cz~+8yy>8|8Zs$3%b2*-h%BD2{j|;83 zJIz3>omQqB8GEL7<-sM^W4`Gz?=d!^c|#Cm77Z z1WJ2(-WNrjD3SZDDj=VR-%pKSijyL8-gzNOa_#OPe#mOt7m^K>G6&7g3-%BSfJ}Sy zh$O|~oN3-`L@xn`gxrzXzi)sNZ9{XFIb@0Qq}A7%KJ#}@WUyH}aA+dA$&j|M6N{Lq-gL0vNFMwh>?dJ(RnNmapHCV}eTQ15PqPErKuDv-%cL^jc}9P@1C~348cB-- z1NF?}s1CiG{*O)qMhW(P9=<*VKcKz`YpxouZ9HXg_Qu|hz$Z)0Oy7)VrHIrz|NRB< z9f^Ca^8_~X{8Vk&TNN!yacS`lh9_mEUVg5Qt1R~q*>7Xs{Yvma{wsB<9tb0Y?Aj1jZ{*)e1(>Mt`Xs@e zi?XwUePyNU{t$bE>?{lB;{k`|Ep}^*BIZU>$=MT;-2FGagO`+zH{dv|B+C&Ptn3cv zC7N~@|L+e#xn-0}`Hhc{)vwc1+EUBGq$Z?vCao0bDzwLc%)Ml4tkxp^%ld6EV@CHu zhL?~te7Sv58=x|T{(T6n-UL^IU4kajAlw{DJBU%9CD zSR>0QC1V`ZtB+0APp40U=HLnh4lln4w<53af%&KTmscu1=8IV2Z{F0}Hw&uRIWBf1QLie=5>;A-u3~W?OlNLztK@ zyio6MIMq_f8>Fyo-T`;RXbiTJM$+B<@F4!Di9;II2{vSEw zPpOfL6|Z{;gP=lOIRcX}zEn#Y3KqHe7;!mm%S74oVIN^2@6LnSXh6dfS5O{0Erph| zqp4)5XZ|6;Q3YqVMruhajBad)TK3c7i6?8@vMIIdL_tjEvJ*ImXhI$xvRBQ_cv-Fe zq*xs^K5{#TRz=ko9EkZ*2|8cH>GBv6W{2D?x0L|L=9ED*{{Gp5r_v%1f6!Ez`(k%#DaWg6q zc07n?4je!DqRy?rt=x+uCju40k?|_iHS?R9$dD6ynTWOc0!^qk5scH;fUc`*AfKNh zBi`25R@T4NUV|(Q*47m`KJ*c#(%?4YVI?Gc=Hzf|_@doP8i)*`zZ?U6$Q=Y_+_i5| zDOps^Jr`>&HFfn5?-{_ni_1&%p(DI@0gfha2164PB8nD=r(JkOi~dujSaSxsBDe{j zS&xe%S>u}}e_3A@Lj%Rh^V-l5qe?*Y?-6let9RWw`OY$H1Y}Z_OiClLTvp#1lNVYboJmZ3rdHE|(w$BO zDsDjMV#d=_09(%~vkY2cO^0bVm;56Z`niy^fLds!58pia>?%!&qzBZ-IcSnS1rOn1 zUWo&U*RLjuI9lZGwI2YJqH`G6JI8JhR72$^jg*6ER=WVPZ zWg8xXXTqUWR>8BG#4R>QIx=+5JC-=xb%sg$!qAd+s1AicJ=_J7o$Q+QWDG`_XlrTi zsDikG3A2j~Yx%7!#!OK(DAOd^m~zknHmj~HyRgt5Dgy6-X#4;nYru?zRNYLX#Uzjw z-SrAJA82ZsF226Lyc3a5W|>W!a2T&=H-r1iM^EJx>_Z$7tl$=vP*LNMX+qkxsOyl? zfPZM7OE|mMSBTA5Os!NJs_gLV0^}{@LV>X+8Aa{Q+emJDSvL` z;W0;>oonOflFU>P%A!skcr_%K%;0N1QgeP>uuJ20$JVu&+?9 z`@%1;1g=+C6PjhRkau7OT4rLJ`6p%WXslU;gQ3?RFc+S~8%^OPnw<;_G4Nw+Wp7zV HqqF}9+v`8C diff --git a/dev-infra/ts-circular-dependencies/file_system.ts b/dev-infra/ts-circular-dependencies/file_system.ts deleted file mode 100644 index 9f5db3199afaa0..00000000000000 --- a/dev-infra/ts-circular-dependencies/file_system.ts +++ /dev/null @@ -1,23 +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 {Stats, statSync} from 'fs'; - -/** Gets the status of the specified file. Returns null if the file does not exist. */ -export function getFileStatus(filePath: string): Stats|null { - try { - return statSync(filePath); - } catch { - return null; - } -} - -/** Ensures that the specified path uses forward slashes as delimiter. */ -export function convertPathToForwardSlash(path: string) { - return path.replace(/\\/g, '/'); -} diff --git a/dev-infra/ts-circular-dependencies/golden.ts b/dev-infra/ts-circular-dependencies/golden.ts deleted file mode 100644 index 1c957332608cf0..00000000000000 --- a/dev-infra/ts-circular-dependencies/golden.ts +++ /dev/null @@ -1,131 +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 {relative} from 'path'; - -import {ReferenceChain} from './analyzer'; -import {convertPathToForwardSlash} from './file_system'; - -export type CircularDependency = ReferenceChain; -export type Golden = CircularDependency[]; - -/** - * Converts a list of reference chains to a JSON-compatible golden object. Reference chains - * by default use TypeScript source file objects. In order to make those chains printable, - * the source file objects are mapped to their relative file names. - */ -export function convertReferenceChainToGolden(refs: ReferenceChain[], baseDir: string): Golden { - return refs - .map( - // Normalize cycles as the paths can vary based on which node in the cycle is visited - // first in the analyzer. The paths represent cycles. Hence we can shift nodes in a - // deterministic way so that the goldens don't change unnecessarily and cycle comparison - // is simpler. - chain => normalizeCircularDependency( - chain.map(({fileName}) => convertPathToForwardSlash(relative(baseDir, fileName))))) - // Sort cycles so that the golden doesn't change unnecessarily when cycles are detected - // in different order (e.g. new imports cause cycles to be detected earlier or later). - .sort(compareCircularDependency); -} - -/** - * Compares the specified goldens and returns two lists that describe newly - * added circular dependencies, or fixed circular dependencies. - */ -export function compareGoldens(actual: Golden, expected: Golden) { - const newCircularDeps: CircularDependency[] = []; - const fixedCircularDeps: CircularDependency[] = []; - actual.forEach(a => { - if (!expected.find(e => isSameCircularDependency(a, e))) { - newCircularDeps.push(a); - } - }); - expected.forEach(e => { - if (!actual.find(a => isSameCircularDependency(e, a))) { - fixedCircularDeps.push(e); - } - }); - return {newCircularDeps, fixedCircularDeps}; -} - -/** - * Normalizes the a circular dependency by ensuring that the path starts with the first - * node in alphabetical order. Since the path array represents a cycle, we can make a - * specific node the first element in the path that represents the cycle. - * - * This method is helpful because the path of circular dependencies changes based on which - * file in the path has been visited first by the analyzer. e.g. Assume we have a circular - * dependency represented as: `A -> B -> C`. The analyzer will detect this cycle when it - * visits `A`. Though when a source file that is analyzed before `A` starts importing `B`, - * the cycle path will detected as `B -> C -> A`. This represents the same cycle, but is just - * different due to a limitation of using a data structure that can be written to a text-based - * golden file. - * - * To account for this non-deterministic behavior in goldens, we shift the circular - * dependency path to the first node based on alphabetical order. e.g. `A` will always - * be the first node in the path that represents the cycle. - */ -function normalizeCircularDependency(path: CircularDependency): CircularDependency { - if (path.length <= 1) { - return path; - } - - let indexFirstNode: number = 0; - let valueFirstNode: string = path[0]; - - // Find a node in the cycle path that precedes all other elements - // in terms of alphabetical order. - for (let i = 1; i < path.length; i++) { - const value = path[i]; - if (value.localeCompare(valueFirstNode, 'en') < 0) { - indexFirstNode = i; - valueFirstNode = value; - } - } - - // If the alphabetically first node is already at start of the path, just - // return the actual path as no changes need to be made. - if (indexFirstNode === 0) { - return path; - } - - // Move the determined first node (as of alphabetical order) to the start of a new - // path array. The nodes before the first node in the old path are then concatenated - // to the end of the new path. This is possible because the path represents a cycle. - return [...path.slice(indexFirstNode), ...path.slice(0, indexFirstNode)]; -} - -/** Checks whether the specified circular dependencies are equal. */ -function isSameCircularDependency(actual: CircularDependency, expected: CircularDependency) { - if (actual.length !== expected.length) { - return false; - } - for (let i = 0; i < actual.length; i++) { - if (actual[i] !== expected[i]) { - return false; - } - } - return true; -} - -/** - * Compares two circular dependencies by respecting the alphabetic order of nodes in the - * cycle paths. The first nodes which don't match in both paths are decisive on the order. - */ -function compareCircularDependency(a: CircularDependency, b: CircularDependency): number { - // Go through nodes in both cycle paths and determine whether `a` should be ordered - // before `b`. The first nodes which don't match decide on the order. - for (let i = 0; i < Math.min(a.length, b.length); i++) { - const compareValue = a[i].localeCompare(b[i], 'en'); - if (compareValue !== 0) { - return compareValue; - } - } - // If all nodes are equal in the cycles, the order is based on the length of both cycles. - return a.length - b.length; -} diff --git a/dev-infra/ts-circular-dependencies/index.ts b/dev-infra/ts-circular-dependencies/index.ts deleted file mode 100644 index 279a281da5b425..00000000000000 --- a/dev-infra/ts-circular-dependencies/index.ts +++ /dev/null @@ -1,135 +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, writeFileSync} from 'fs'; -import {sync as globSync} from 'glob'; -import {isAbsolute, relative, resolve} from 'path'; -import * as ts from 'typescript'; -import * as yargs from 'yargs'; - -import {error, green, info, red, yellow} from '../utils/console'; - -import {Analyzer, ReferenceChain} from './analyzer'; -import {CircularDependenciesTestConfig, loadTestConfig} from './config'; -import {convertPathToForwardSlash} from './file_system'; -import {compareGoldens, convertReferenceChainToGolden, Golden} from './golden'; - - -export function tsCircularDependenciesBuilder(localYargs: yargs.Argv) { - return localYargs.help() - .strict() - .demandCommand() - .option( - 'config', - {type: 'string', demandOption: true, description: 'Path to the configuration file.'}) - .option('warnings', {type: 'boolean', description: 'Prints all warnings.'}) - .command( - 'check', 'Checks if the circular dependencies have changed.', args => args, - argv => { - const {config: configArg, warnings} = argv; - const configPath = isAbsolute(configArg) ? configArg : resolve(configArg); - const config = loadTestConfig(configPath); - process.exit(main(false, config, !!warnings)); - }) - .command('approve', 'Approves the current circular dependencies.', args => args, argv => { - const {config: configArg, warnings} = argv; - const configPath = isAbsolute(configArg) ? configArg : resolve(configArg); - const config = loadTestConfig(configPath); - process.exit(main(true, config, !!warnings)); - }); -} - -/** - * Runs the ts-circular-dependencies tool. - * @param approve Whether the detected circular dependencies should be approved. - * @param config Configuration for the current circular dependencies test. - * @param printWarnings Whether warnings should be printed out. - * @returns Status code. - */ -export function main( - approve: boolean, config: CircularDependenciesTestConfig, printWarnings: boolean): number { - const {baseDir, goldenFile, glob, resolveModule, approveCommand} = config; - const analyzer = new Analyzer(resolveModule); - const cycles: ReferenceChain[] = []; - const checkedNodes = new WeakSet(); - - globSync(glob, {absolute: true, ignore: ['**/node_modules/**']}).forEach(filePath => { - const sourceFile = analyzer.getSourceFile(filePath); - cycles.push(...analyzer.findCycles(sourceFile, checkedNodes)); - }); - - const actual = convertReferenceChainToGolden(cycles, baseDir); - - info(green(` Current number of cycles: ${yellow(cycles.length.toString())}`)); - - if (approve) { - writeFileSync(goldenFile, JSON.stringify(actual, null, 2)); - info(green('✅ Updated golden file.')); - return 0; - } else if (!existsSync(goldenFile)) { - error(red(`❌ Could not find golden file: ${goldenFile}`)); - return 1; - } - - const warningsCount = analyzer.unresolvedFiles.size + analyzer.unresolvedModules.size; - - // By default, warnings for unresolved files or modules are not printed. This is because - // it's common that third-party modules are not resolved/visited. Also generated files - // from the View Engine compiler (i.e. factories, summaries) cannot be resolved. - if (printWarnings && warningsCount !== 0) { - info(yellow('⚠ The following imports could not be resolved:')); - Array.from(analyzer.unresolvedModules).sort().forEach(specifier => info(` • ${specifier}`)); - analyzer.unresolvedFiles.forEach((value, key) => { - info(` • ${getRelativePath(baseDir, key)}`); - value.sort().forEach(specifier => info(` ${specifier}`)); - }); - } else { - info(yellow(`⚠ ${warningsCount} imports could not be resolved.`)); - info(yellow(` Please rerun with "--warnings" to inspect unresolved imports.`)); - } - - const expected = JSON.parse(readFileSync(goldenFile, 'utf8')) as Golden; - const {fixedCircularDeps, newCircularDeps} = compareGoldens(actual, expected); - const isMatching = fixedCircularDeps.length === 0 && newCircularDeps.length === 0; - - if (isMatching) { - info(green('✅ Golden matches current circular dependencies.')); - return 0; - } - - error(red('❌ Golden does not match current circular dependencies.')); - if (newCircularDeps.length !== 0) { - error(yellow(` New circular dependencies which are not allowed:`)); - newCircularDeps.forEach(c => error(` • ${convertReferenceChainToString(c)}`)); - error(); - } - if (fixedCircularDeps.length !== 0) { - error(yellow(` Fixed circular dependencies that need to be removed from the golden:`)); - fixedCircularDeps.forEach(c => error(` • ${convertReferenceChainToString(c)}`)); - info(yellow(`\n Total: ${newCircularDeps.length} new cycle(s), ${ - fixedCircularDeps.length} fixed cycle(s). \n`)); - if (approveCommand) { - info(yellow(` Please approve the new golden with: ${approveCommand}`)); - } else { - info(yellow( - ` Please update the golden. The following command can be ` + - `run: yarn ts-circular-deps approve ${getRelativePath(process.cwd(), goldenFile)}.`)); - } - } - return 1; -} - -/** Gets the specified path relative to the base directory. */ -function getRelativePath(baseDir: string, path: string) { - return convertPathToForwardSlash(relative(baseDir, path)); -} - -/** Converts the given reference chain to its string representation. */ -function convertReferenceChainToString(chain: ReferenceChain) { - return chain.join(' → '); -} diff --git a/dev-infra/ts-circular-dependencies/parser.ts b/dev-infra/ts-circular-dependencies/parser.ts deleted file mode 100644 index dc7b85b945c3dc..00000000000000 --- a/dev-infra/ts-circular-dependencies/parser.ts +++ /dev/null @@ -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 * as ts from 'typescript'; - -/** - * Finds all module references in the specified source file. - * @param node Source file which should be parsed. - * @returns List of import specifiers in the source file. - */ -export function getModuleReferences(node: ts.SourceFile): string[] { - const references: string[] = []; - const visitNode = (node: ts.Node) => { - if ((ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) && - node.moduleSpecifier !== undefined && ts.isStringLiteral(node.moduleSpecifier)) { - references.push(node.moduleSpecifier.text); - } - ts.forEachChild(node, visitNode); - }; - ts.forEachChild(node, visitNode); - return references; -} diff --git a/dev-infra/tsconfig.json b/dev-infra/tsconfig.json deleted file mode 100644 index b5c32c096ce0c8..00000000000000 --- a/dev-infra/tsconfig.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "compilerOptions": { - "strict": true, - "downlevelIteration": true, - "noImplicitAny": true, - "noImplicitOverride": true - } -} diff --git a/dev-infra/tslint-rules/BUILD.bazel b/dev-infra/tslint-rules/BUILD.bazel deleted file mode 100644 index 7d7baf8d33e7a6..00000000000000 --- a/dev-infra/tslint-rules/BUILD.bazel +++ /dev/null @@ -1,13 +0,0 @@ -load("@npm//@bazel/typescript:index.bzl", "ts_library") - -ts_library( - name = "tslint-rules", - srcs = glob(["*.ts"]), - module_name = "@angular/dev-infra-private/tslint-rules", - visibility = ["//dev-infra:__subpackages__"], - deps = [ - "@npm//tslib", - "@npm//tslint", - "@npm//typescript", - ], -) diff --git a/dev-infra/tslint-rules/noImplicitOverrideAbstractRule.ts b/dev-infra/tslint-rules/noImplicitOverrideAbstractRule.ts deleted file mode 100644 index 73ad2f9fc6dd9d..00000000000000 --- a/dev-infra/tslint-rules/noImplicitOverrideAbstractRule.ts +++ /dev/null @@ -1,145 +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 {Replacement, RuleFailure, WalkContext} from 'tslint/lib'; -import {TypedRule} from 'tslint/lib/rules'; -import * as ts from 'typescript'; - -const FAILURE_MESSAGE = 'Missing override modifier. Members implemented as part of ' + - 'abstract classes must explicitly set the "override" modifier. ' + - 'More details: https://github.com/microsoft/TypeScript/issues/44457#issuecomment-856202843.'; - -/** - * Rule which enforces that class members implementing abstract members - * from base classes explicitly specify the `override` modifier. - * - * This ensures we follow the best-practice of applying `override` for abstract-implemented - * members so that TypeScript creates diagnostics in both scenarios where either the abstract - * class member is removed, or renamed. - * - * More details can be found here: https://github.com/microsoft/TypeScript/issues/44457. - */ -export class Rule extends TypedRule { - override applyWithProgram(sourceFile: ts.SourceFile, program: ts.Program): RuleFailure[] { - return this.applyWithFunction(sourceFile, ctx => visitNode(sourceFile, ctx, program)); - } -} - -/** - * For a TypeScript AST node and each of its child nodes, check whether the node is a class - * element which implements an abstract member but does not have the `override` keyword. - */ -function visitNode(node: ts.Node, ctx: WalkContext, program: ts.Program) { - // If a class element implements an abstract member but does not have the - // `override` keyword, create a lint failure. - if (ts.isClassElement(node) && !hasOverrideModifier(node) && - matchesParentAbstractElement(node, program)) { - ctx.addFailureAtNode( - node, FAILURE_MESSAGE, Replacement.appendText(node.getStart(), `override `)); - } - - ts.forEachChild(node, node => visitNode(node, ctx, program)); -} - -/** - * Checks if the specified class element matches a parent abstract class element. i.e. - * whether the specified member "implements" an abstract member from a base class. - */ -function matchesParentAbstractElement(node: ts.ClassElement, program: ts.Program): boolean { - const containingClass = node.parent as ts.ClassDeclaration; - - // If the property we check does not have a property name, we cannot look for similarly-named - // members in parent classes and therefore return early. - if (node.name === undefined) { - return false; - } - - const propertyName = getPropertyNameText(node.name); - const typeChecker = program.getTypeChecker(); - - // If the property we check does not have a statically-analyzable property name, - // we cannot look for similarly-named members in parent classes and return early. - if (propertyName === null) { - return false; - } - - return checkClassForInheritedMatchingAbstractMember(containingClass, typeChecker, propertyName); -} - -/** Checks if the given class inherits an abstract member with the specified name. */ -function checkClassForInheritedMatchingAbstractMember( - clazz: ts.ClassDeclaration, typeChecker: ts.TypeChecker, searchMemberName: string): boolean { - const baseClass = getBaseClass(clazz, typeChecker); - - // If the class is not `abstract`, then all parent abstract methods would need to - // be implemented, and there is never an abstract member within the class. - if (baseClass === null || !hasAbstractModifier(baseClass)) { - return false; - } - - const matchingMember = baseClass.members.find( - m => m.name !== undefined && getPropertyNameText(m.name) === searchMemberName); - - if (matchingMember !== undefined) { - return hasAbstractModifier(matchingMember); - } - - return checkClassForInheritedMatchingAbstractMember(baseClass, typeChecker, searchMemberName); -} - -/** Gets the base class for the given class declaration. */ -function getBaseClass(node: ts.ClassDeclaration, typeChecker: ts.TypeChecker): ts.ClassDeclaration| - null { - const baseTypes = getExtendsHeritageExpressions(node); - - if (baseTypes.length > 1) { - throw Error('Class unexpectedly extends from multiple types.'); - } - - const baseClass = typeChecker.getTypeAtLocation(baseTypes[0]).getSymbol(); - const baseClassDecl = baseClass?.valueDeclaration ?? baseClass?.declarations?.[0]; - - if (baseClassDecl !== undefined && ts.isClassDeclaration(baseClassDecl)) { - return baseClassDecl; - } - - return null; -} - -/** Gets the `extends` base type expressions of the specified class. */ -function getExtendsHeritageExpressions(classDecl: ts.ClassDeclaration): - ts.ExpressionWithTypeArguments[] { - if (classDecl.heritageClauses === undefined) { - return []; - } - const result: ts.ExpressionWithTypeArguments[] = []; - for (const clause of classDecl.heritageClauses) { - if (clause.token === ts.SyntaxKind.ExtendsKeyword) { - result.push(...clause.types); - } - } - return result; -} - -/** Gets whether the specified node has the `abstract` modifier applied. */ -function hasAbstractModifier(node: ts.Node): boolean { - return !!node.modifiers?.some(s => s.kind === ts.SyntaxKind.AbstractKeyword); -} - -/** Gets whether the specified node has the `override` modifier applied. */ -function hasOverrideModifier(node: ts.Node): boolean { - return !!node.modifiers?.some(s => s.kind === ts.SyntaxKind.OverrideKeyword); -} - -/** Gets the property name text of the specified property name. */ -function getPropertyNameText(name: ts.PropertyName): string|null { - if (ts.isComputedPropertyName(name)) { - return null; - } - return name.text; -} diff --git a/dev-infra/utils/BUILD.bazel b/dev-infra/utils/BUILD.bazel deleted file mode 100644 index cc51124606a881..00000000000000 --- a/dev-infra/utils/BUILD.bazel +++ /dev/null @@ -1,32 +0,0 @@ -load("//dev-infra:defaults.bzl", "ts_library") - -ts_library( - name = "utils", - srcs = glob([ - "*.ts", - "git/*.ts", - ]), - - # prodmode target must be set to es5 as the Octokit class, which is extended is not a proper - # prototyped object. - prodmode_target = "es5", - visibility = ["//dev-infra:__subpackages__"], - deps = [ - "@npm//@octokit/core", - "@npm//@octokit/graphql", - "@npm//@octokit/plugin-paginate-rest", - "@npm//@octokit/plugin-rest-endpoint-methods", - "@npm//@octokit/rest", - "@npm//@octokit/types", - "@npm//@types/inquirer", - "@npm//@types/node", - "@npm//@types/semver", - "@npm//@types/yargs", - "@npm//chalk", - "@npm//inquirer", - "@npm//semver", - "@npm//tslib", - "@npm//typed-graphqlify", - "@npm//yargs", - ], -) diff --git a/dev-infra/utils/child-process.ts b/dev-infra/utils/child-process.ts deleted file mode 100644 index 809b39baefc103..00000000000000 --- a/dev-infra/utils/child-process.ts +++ /dev/null @@ -1,151 +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 {spawn as _spawn, SpawnOptions as _SpawnOptions, spawnSync as _spawnSync, SpawnSyncOptions as _SpawnSyncOptions} from 'child_process'; -import {debug, error} from './console'; - - -/** Interface describing the options for spawning a process synchronously. */ -export interface SpawnSyncOptions extends Omit<_SpawnSyncOptions, 'shell'|'stdio'> { - /** Whether to prevent exit codes being treated as failures. */ - suppressErrorOnFailingExitCode?: boolean; -} - -/** Interface describing the options for spawning a process. */ -export interface SpawnOptions extends Omit<_SpawnOptions, 'shell'|'stdio'> { - /** Console output mode. Defaults to "enabled". */ - mode?: 'enabled'|'silent'|'on-error'; - /** Whether to prevent exit codes being treated as failures. */ - suppressErrorOnFailingExitCode?: boolean; -} - -/** Interface describing the options for spawning an interactive process. */ -export type SpawnInteractiveCommandOptions = Omit<_SpawnOptions, 'shell'|'stdio'>; - -/** Interface describing the result of a spawned process. */ -export interface SpawnResult { - /** Captured stdout in string format. */ - stdout: string; - /** Captured stderr in string format. */ - stderr: string; - /** The exit code or signal of the process. */ - status: number|NodeJS.Signals; -} - -/** - * Spawns a given command with the specified arguments inside an interactive shell. All process - * stdin, stdout and stderr output is printed to the current console. - * - * @returns a Promise resolving on success, and rejecting on command failure with the status code. - */ -export function spawnInteractive( - command: string, args: string[], options: SpawnInteractiveCommandOptions = {}) { - return new Promise((resolve, reject) => { - const commandText = `${command} ${args.join(' ')}`; - debug(`Executing command: ${commandText}`); - const childProcess = _spawn(command, args, {...options, shell: true, stdio: 'inherit'}); - childProcess.on('exit', status => status === 0 ? resolve() : reject(status)); - }); -} - -/** - * Spawns a given command with the specified arguments inside a shell. All process stdout - * output is captured and returned as resolution on completion. Depending on the chosen - * output mode, stdout/stderr output is also printed to the console, or only on error. - * - * @returns a Promise resolving with captured stdout and stderr on success. The promise - * rejects on command failure. - */ -export function spawn( - command: string, args: string[], options: SpawnOptions = {}): Promise { - return new Promise((resolve, reject) => { - const commandText = `${command} ${args.join(' ')}`; - const outputMode = options.mode; - - debug(`Executing command: ${commandText}`); - - const childProcess = _spawn(command, args, {...options, shell: true, stdio: 'pipe'}); - let logOutput = ''; - let stdout = ''; - let stderr = ''; - - // Capture the stdout separately so that it can be passed as resolve value. - // This is useful if commands return parsable stdout. - childProcess.stderr.on('data', message => { - stderr += message; - logOutput += message; - // If console output is enabled, print the message directly to the stderr. Note that - // we intentionally print all output to stderr as stdout should not be polluted. - if (outputMode === undefined || outputMode === 'enabled') { - process.stderr.write(message); - } - }); - - childProcess.stdout.on('data', message => { - stdout += message; - logOutput += message; - // If console output is enabled, print the message directly to the stderr. Note that - // we intentionally print all output to stderr as stdout should not be polluted. - if (outputMode === undefined || outputMode === 'enabled') { - process.stderr.write(message); - } - }); - - childProcess.on('exit', (exitCode, signal) => { - const exitDescription = exitCode !== null ? `exit code "${exitCode}"` : `signal "${signal}"`; - const printFn = outputMode === 'on-error' ? error : debug; - const status = statusFromExitCodeAndSignal(exitCode, signal); - - printFn(`Command "${commandText}" completed with ${exitDescription}.`); - printFn(`Process output: \n${logOutput}`); - - // On success, resolve the promise. Otherwise reject with the captured stderr - // and stdout log output if the output mode was set to `silent`. - if (status === 0 || options.suppressErrorOnFailingExitCode) { - resolve({stdout, stderr, status}); - } else { - reject(outputMode === 'silent' ? logOutput : undefined); - } - }); - }); -} - -/** - * Spawns a given command with the specified arguments inside a shell synchronously. - * - * @returns The command's stdout and stderr. - */ -export function spawnSync( - command: string, args: string[], options: SpawnSyncOptions = {}): SpawnResult { - const commandText = `${command} ${args.join(' ')}`; - debug(`Executing command: ${commandText}`); - - const {status: exitCode, signal, stdout, stderr} = - _spawnSync(command, args, {...options, encoding: 'utf8', shell: true, stdio: 'pipe'}); - - /** The status of the spawn result. */ - const status = statusFromExitCodeAndSignal(exitCode, signal); - - if (status === 0 || options.suppressErrorOnFailingExitCode) { - return {status, stdout, stderr}; - } - - throw new Error(stderr); -} - -/** - * Convert the provided exitCode and signal to a single status code. - * - * During `exit` node provides either a `code` or `signal`, one of which is guaranteed to be - * non-null. - * - * For more details see: https://nodejs.org/api/child_process.html#child_process_event_exit - */ -function statusFromExitCodeAndSignal(exitCode: number|null, signal: NodeJS.Signals|null) { - return exitCode ?? signal ?? -1; -} diff --git a/dev-infra/utils/config.ts b/dev-infra/utils/config.ts deleted file mode 100644 index ca9b25e14ff8ec..00000000000000 --- a/dev-infra/utils/config.ts +++ /dev/null @@ -1,166 +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} from 'fs'; -import {dirname, join} from 'path'; - -import {debug, error} from './console'; -import {GitClient} from './git/git-client'; -import {isTsNodeAvailable} from './ts-node'; - -/** Configuration for Git client interactions. */ -export interface GitClientConfig { - /** Owner name of the repository. */ - owner: string; - /** Name of the repository. */ - name: string; - /** If SSH protocol should be used for git interactions. */ - useSsh?: boolean; - /** Whether the specified repository is private. */ - private?: boolean; -} - -/** - * Describes the Github configuration for dev-infra. This configuration is - * used for API requests, determining the upstream remote, etc. - */ -export interface GithubConfig extends GitClientConfig {} - -/** The common configuration for ng-dev. */ -type CommonConfig = { - github: GithubConfig -}; - -/** - * The configuration for the specific ng-dev command, providing both the common - * ng-dev config as well as the specific config of a subcommand. - */ -export type NgDevConfig = CommonConfig&T; - -/** - * The filename expected for creating the ng-dev config, without the file - * extension to allow either a typescript or javascript file to be used. - */ -const CONFIG_FILE_PATH = '.ng-dev/config'; - -/** The configuration for ng-dev. */ -let cachedConfig: NgDevConfig|null = null; - -/** - * The filename expected for local user config, without the file extension to allow a typescript, - * javascript or json file to be used. - */ -const USER_CONFIG_FILE_PATH = '.ng-dev.user'; - -/** The local user configuration for ng-dev. */ -let userConfig: {[key: string]: any}|null = null; - -/** - * Get the configuration from the file system, returning the already loaded - * copy if it is defined. - */ -export function getConfig(): NgDevConfig; -export function getConfig(baseDir?: string): NgDevConfig; -export function getConfig(baseDir?: string): NgDevConfig { - // 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. - const configPath = 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 {...cachedConfig}; -} - -/** Validate the common configuration has been met for the ng-dev command. */ -function validateCommonConfig(config: Partial) { - const errors: string[] = []; - // 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 as NgDevConfig; -} - -/** - * Resolves and reads the specified configuration file, optionally returning an empty object if the - * configuration file cannot be read. - */ -function readConfigFile(configPath: string, returnEmptyObjectOnError = false): object { - // 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 && 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: 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. - */ -export function assertNoErrors(errors: string[]) { - if (errors.length == 0) { - return; - } - error(`Errors discovered while loading configuration file:`); - for (const err of errors) { - error(` - ${err}`); - } - process.exit(1); -} - -/** - * Get the local user configuration from the file system, returning the already loaded copy if it is - * defined. - * - * @returns The user configuration object, or an empty object if no user configuration file is - * present. The object is an untyped object as there are no required user configurations. - */ -export function getUserConfig() { - // If the global config is not defined, load it from the file system. - if (userConfig === null) { - const git = GitClient.get(); - // The full path to the configuration file. - const configPath = join(git.baseDir, USER_CONFIG_FILE_PATH); - // Set the global config object. - userConfig = readConfigFile(configPath, true); - } - // Return a clone of the user config to ensure that a new instance of the config is returned - // each time, preventing unexpected effects of modifications to the config object. - return {...userConfig}; -} diff --git a/dev-infra/utils/console.ts b/dev-infra/utils/console.ts deleted file mode 100644 index b24f722f75bc7d..00000000000000 --- a/dev-infra/utils/console.ts +++ /dev/null @@ -1,184 +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 chalk from 'chalk'; -import {writeFileSync} from 'fs'; -import {prompt} from 'inquirer'; -import {join} from 'path'; -import {Arguments} from 'yargs'; - -import {GitClient} from './git/git-client'; - -/** Reexport of chalk colors for convenient access. */ -export const red = chalk.red; -export const green = chalk.green; -export const yellow = chalk.yellow; -export const bold = chalk.bold; -export const blue = chalk.blue; - -/** Prompts the user with a confirmation question and a specified message. */ -export async function promptConfirm(message: string, defaultValue = false): Promise { - return (await prompt<{result: boolean}>({ - type: 'confirm', - name: 'result', - message: message, - default: defaultValue, - })) - .result; -} - -/** Prompts the user for one line of input. */ -export async function promptInput(message: string): Promise { - return (await prompt<{result: string}>({type: 'input', name: 'result', message})).result; -} - -/** - * Supported levels for logging functions. - * - * Levels are mapped to numbers to represent a hierarchy of logging levels. - */ -export enum LOG_LEVELS { - SILENT = 0, - ERROR = 1, - WARN = 2, - LOG = 3, - INFO = 4, - DEBUG = 5, -} - -/** Default log level for the tool. */ -export const DEFAULT_LOG_LEVEL = LOG_LEVELS.INFO; - -/** Write to the console for at INFO logging level */ -export const info = buildLogLevelFunction(() => console.info, LOG_LEVELS.INFO); - -/** Write to the console for at ERROR logging level */ -export const error = buildLogLevelFunction(() => console.error, LOG_LEVELS.ERROR); - -/** Write to the console for at DEBUG logging level */ -export const debug = buildLogLevelFunction(() => console.debug, LOG_LEVELS.DEBUG); - -/** Write to the console for at LOG logging level */ -// tslint:disable-next-line: no-console -export const log = buildLogLevelFunction(() => console.log, LOG_LEVELS.LOG); - -/** Write to the console for at WARN logging level */ -export const warn = buildLogLevelFunction(() => console.warn, LOG_LEVELS.WARN); - -/** Build an instance of a logging function for the provided level. */ -function buildLogLevelFunction(loadCommand: () => Function, level: LOG_LEVELS) { - /** Write to stdout for the LOG_LEVEL. */ - const loggingFunction = (...text: string[]) => { - runConsoleCommand(loadCommand, level, ...text); - }; - - /** Start a group at the LOG_LEVEL, optionally starting it as collapsed. */ - loggingFunction.group = (text: string, collapsed = false) => { - const command = collapsed ? console.groupCollapsed : console.group; - runConsoleCommand(() => command, level, text); - }; - - /** End the group at the LOG_LEVEL. */ - loggingFunction.groupEnd = () => { - runConsoleCommand(() => 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: () => Function, logLevel: LOG_LEVELS, ...text: string[]) { - if (getLogLevel() >= logLevel) { - loadCommand()(...text); - } - printToLogFile(logLevel, ...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() { - const logLevelEnvValue: any = (process.env[`LOG_LEVEL`] || '').toUpperCase(); - const logLevel = LOG_LEVELS[logLevelEnvValue]; - if (logLevel === undefined) { - return DEFAULT_LOG_LEVEL; - } - return logLevel; -} - -/** All text to write to the log file. */ -let LOGGED_TEXT = ''; -/** Whether file logging as been enabled. */ -let FILE_LOGGING_ENABLED = false; -/** - * The number of columns used in the prepended log level information on each line of the logging - * output file. - */ -const LOG_LEVEL_COLUMNS = 7; - -/** - * Enable writing the logged outputs to the log file on process exit, sets initial lines from the - * command execution, containing information about the timing and command parameters. - * - * This is expected to be called only once during a command run, and should be called by the - * middleware of yargs to enable the file logging before the rest of the command parsing and - * response is executed. - */ -export function captureLogOutputForCommand(argv: Arguments) { - if (FILE_LOGGING_ENABLED) { - throw Error('`captureLogOutputForCommand` cannot be called multiple times'); - } - - const git = GitClient.get(); - /** The date time used for timestamping when the command was invoked. */ - const now = new Date(); - /** Header line to separate command runs in log files. */ - const headerLine = Array(100).fill('#').join(''); - LOGGED_TEXT += `${headerLine}\nCommand: ${argv.$0} ${argv._.join(' ')}\nRan at: ${now}\n`; - - // On process exit, write the logged output to the appropriate log files - process.on('exit', (code: number) => { - LOGGED_TEXT += `${headerLine}\n`; - LOGGED_TEXT += `Command ran in ${new Date().getTime() - now.getTime()}ms\n`; - LOGGED_TEXT += `Exit Code: ${code}\n`; - /** Path to the log file location. */ - const logFilePath = join(git.baseDir, '.ng-dev.log'); - - // Strip ANSI escape codes from log outputs. - LOGGED_TEXT = LOGGED_TEXT.replace(/\x1B\[([0-9]{1,3}(;[0-9]{1,2})?)?[mGK]/g, ''); - - writeFileSync(logFilePath, LOGGED_TEXT); - - // For failure codes greater than 1, the new logged lines should be written to a specific log - // file for the command run failure. - if (code > 1) { - const logFileName = `.ng-dev.err-${now.getTime()}.log`; - console.error(`Exit code: ${code}. Writing full log to ${logFileName}`); - writeFileSync(join(git.baseDir, logFileName), LOGGED_TEXT); - } - }); - - // Mark file logging as enabled to prevent the function from executing multiple times. - FILE_LOGGING_ENABLED = true; -} - -/** Write the provided text to the log file, prepending each line with the log level. */ -function printToLogFile(logLevel: LOG_LEVELS, ...text: string[]) { - const logLevelText = `${LOG_LEVELS[logLevel]}:`.padEnd(LOG_LEVEL_COLUMNS); - LOGGED_TEXT += text.join(' ').split('\n').map(l => `${logLevelText} ${l}\n`).join(''); -} diff --git a/dev-infra/utils/dry-run.ts b/dev-infra/utils/dry-run.ts deleted file mode 100644 index e1358e4af2ce5a..00000000000000 --- a/dev-infra/utils/dry-run.ts +++ /dev/null @@ -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 - */ - -import {Argv} from 'yargs'; - -/** - * Add a --dry-run flag to the available options for the yargs argv object. When present, sets an - * environment variable noting dry run mode. - */ -export function addDryRunFlag(args: Argv) { - return args.option('dry-run' as 'dryRun', { - type: 'boolean', - default: false, - description: 'Whether to do a dry run', - coerce: (dryRun: boolean) => { - if (dryRun) { - process.env['DRY_RUN'] = '1'; - } - return dryRun; - } - }); -} - -/** Whether the current environment is in dry run mode. */ -export function isDryRun(): boolean { - return process.env['DRY_RUN'] !== undefined; -} - -/** Error to be thrown when a function or method is called in dryRun mode and shouldn't be. */ -export class DryRunError extends Error { - constructor() { - super('Cannot call this function in dryRun mode.'); - // 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); - } -} diff --git a/dev-infra/utils/git/authenticated-git-client.ts b/dev-infra/utils/git/authenticated-git-client.ts deleted file mode 100644 index 7cc3987b328dab..00000000000000 --- a/dev-infra/utils/git/authenticated-git-client.ts +++ /dev/null @@ -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 {NgDevConfig} from '../config'; -import {yellow} from '../console'; - -import {GitClient} from './git-client'; -import {AuthenticatedGithubClient} from './github'; -import {getRepositoryGitUrl, GITHUB_TOKEN_GENERATE_URL, GITHUB_TOKEN_SETTINGS_URL} from './github-urls'; - -/** Describes a function that can be used to test for given Github OAuth scopes. */ -export type OAuthScopeTestFunction = (scopes: string[], missing: string[]) => void; - -/** - * Extension of the `GitClient` with additional utilities which are useful for - * authenticated Git client instances. - */ -export class AuthenticatedGitClient extends GitClient { - /** - * Regular expression that matches the provided Github token. Used for - * sanitizing the token from Git child process output. - */ - private readonly _githubTokenRegex: RegExp = new RegExp(this.githubToken, 'g'); - - /** The OAuth scopes available for the provided Github token. */ - private _cachedOauthScopes: Promise|null = null; - - /** Instance of an authenticated github client. */ - override readonly github = new AuthenticatedGithubClient(this.githubToken); - - protected constructor(readonly githubToken: string, baseDir?: string, config?: NgDevConfig) { - super(baseDir, config); - } - - /** Sanitizes a given message by omitting the provided Github token if present. */ - override sanitizeConsoleOutput(value: string): string { - return value.replace(this._githubTokenRegex, ''); - } - - /** Git URL that resolves to the configured repository. */ - override getRepoGitUrl() { - return getRepositoryGitUrl(this.remoteConfig, this.githubToken); - } - - /** - * Assert the GitClient instance is using a token with permissions for the all of the - * provided OAuth scopes. - */ - async hasOauthScopes(testFn: OAuthScopeTestFunction): Promise { - const scopes = await this._fetchAuthScopesForToken(); - const missingScopes: string[] = []; - // Test Github OAuth scopes and collect missing ones. - testFn(scopes, missingScopes); - // If no missing scopes are found, return true to indicate all OAuth Scopes are available. - if (missingScopes.length === 0) { - return true; - } - - // Pre-constructed error message to log to the user, providing missing scopes and - // remediation instructions. - const error = - `The provided does not have required permissions due to missing scope(s): ` + - `${yellow(missingScopes.join(', '))}\n\n` + - `Update the token in use at:\n` + - ` ${GITHUB_TOKEN_SETTINGS_URL}\n\n` + - `Alternatively, a new token can be created at: ${GITHUB_TOKEN_GENERATE_URL}\n`; - - return {error}; - } - - /** Fetch the OAuth scopes for the loaded Github token. */ - private _fetchAuthScopesForToken() { - // If the OAuth scopes have already been loaded, return the Promise containing them. - if (this._cachedOauthScopes !== null) { - return this._cachedOauthScopes; - } - // OAuth scopes are loaded via the /rate_limit endpoint to prevent - // usage of a request against that rate_limit for this lookup. - return this._cachedOauthScopes = this.github.rateLimit.get().then(response => { - const scopes = response.headers['x-oauth-scopes']; - - // If no token is provided, or if the Github client is authenticated incorrectly, - // the `x-oauth-scopes` response header is not set. We error in such cases as it - // signifies a faulty of the - if (scopes === undefined) { - throw Error('Unable to retrieve OAuth scopes for token provided to Git client.'); - } - - return scopes.split(',').map(scope => scope.trim()).filter(scope => scope !== ''); - }); - } - - /** The singleton instance of the `AuthenticatedGitClient`. */ - private static _authenticatedInstance: AuthenticatedGitClient; - - /** - * Static method to get the singleton instance of the `AuthenticatedGitClient`, - * creating it if it has not yet been created. - */ - static override get(): AuthenticatedGitClient { - if (!AuthenticatedGitClient._authenticatedInstance) { - throw new Error('No instance of `AuthenticatedGitClient` has been set up yet.'); - } - return AuthenticatedGitClient._authenticatedInstance; - } - - /** Configures an authenticated git client. */ - static configure(token: string): void { - if (AuthenticatedGitClient._authenticatedInstance) { - throw Error( - 'Unable to configure `AuthenticatedGitClient` as it has been configured already.'); - } - AuthenticatedGitClient._authenticatedInstance = new AuthenticatedGitClient(token); - } -} diff --git a/dev-infra/utils/git/git-client.ts b/dev-infra/utils/git/git-client.ts deleted file mode 100644 index e66f85ed1d9f65..00000000000000 --- a/dev-infra/utils/git/git-client.ts +++ /dev/null @@ -1,256 +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 {spawnSync, SpawnSyncOptions, SpawnSyncReturns} from 'child_process'; -import {Options as SemVerOptions, parse, SemVer} from 'semver'; - -import {getConfig, GithubConfig, NgDevConfig} from '../config'; -import {debug, info} from '../console'; -import {DryRunError, isDryRun} from '../dry-run'; - -import {GithubClient} from './github'; -import {getRepositoryGitUrl} from './github-urls'; - -/** Error for failed Git commands. */ -export class GitCommandError extends Error { - constructor(client: GitClient, public args: string[]) { - // 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(`Command failed: git ${client.sanitizeConsoleOutput(args.join(' '))}`); - - // 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); - } -} - -/** The options available for the `GitClient``run` and `runGraceful` methods. */ -type GitCommandRunOptions = SpawnSyncOptions&{ - verboseLogging?: boolean; -}; - -/** Class that can be used to perform Git interactions with a given remote. **/ -export class GitClient { - /** Short-hand for accessing the default remote configuration. */ - readonly remoteConfig: GithubConfig = this.config.github; - - /** Octokit request parameters object for targeting the configured remote. */ - readonly remoteParams = {owner: this.remoteConfig.owner, repo: this.remoteConfig.name}; - - /** Instance of the Github client. */ - readonly github = new GithubClient(); - - constructor( - /** The full path to the root of the repository base. */ - readonly baseDir = determineRepoBaseDirFromCwd(), - /** The configuration, containing the github specific configuration. */ - readonly config = getConfig(baseDir)) {} - - /** Executes the given git command. Throws if the command fails. */ - run(args: string[], options?: GitCommandRunOptions): Omit, 'status'> { - const 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 as Omit, 'status'>; - } - - /** - * 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. - */ - runGraceful(args: string[], options: GitCommandRunOptions = {}): SpawnSyncReturns { - /** The git command to be run. */ - const 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. - const 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(' '))); - - const result = spawnSync('git', args, { - 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. */ - getRepoGitUrl() { - return getRepositoryGitUrl(this.remoteConfig); - } - - /** Whether the given branch contains the specified SHA. */ - hasCommit(branchName: string, sha: string): boolean { - return this.run(['branch', branchName, '--contains', sha]).stdout !== ''; - } - - /** Gets the currently checked out branch or revision. */ - getCurrentBranchOrRevision(): string { - const 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. */ - hasUncommittedChanges(): boolean { - 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. - */ - checkout(branchOrRevision: string, cleanState: boolean): boolean { - 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. */ - getLatestSemverTag(): SemVer { - const semVerOptions: SemVerOptions = {loose: true}; - const tags = this.runGraceful(['tag', '--sort=-committerdate', '--merged']).stdout.split('\n'); - const latestTag = tags.find((tag: string) => parse(tag, semVerOptions)); - - if (latestTag === undefined) { - throw new Error( - `Unable to find a SemVer matching tag on "${this.getCurrentBranchOrRevision()}"`); - } - return new SemVer(latestTag, semVerOptions); - } - - /** Retrieves the git tag matching the provided SemVer, if it exists. */ - getMatchingTagForSemver(semver: SemVer): string { - const semVerOptions: SemVerOptions = {loose: true}; - const tags = this.runGraceful(['tag', '--sort=-committerdate', '--merged']).stdout.split('\n'); - const matchingTag = - tags.find((tag: string) => parse(tag, semVerOptions)?.compare(semver) === 0); - - if (matchingTag === undefined) { - throw new Error(`Unable to find a tag for the version: "${semver.format()}"`); - } - return matchingTag; - } - - /** Retrieve a list of all files in the repository changed since the provided shaOrRef. */ - allChangesFilesSince(shaOrRef = 'HEAD'): string[] { - return Array.from(new Set([ - ...gitOutputAsArray(this.runGraceful(['diff', '--name-only', '--diff-filter=d', shaOrRef])), - ...gitOutputAsArray(this.runGraceful(['ls-files', '--others', '--exclude-standard'])), - ])); - } - - /** Retrieve a list of all files currently staged in the repostitory. */ - allStagedFiles(): string[] { - return gitOutputAsArray( - this.runGraceful(['diff', '--name-only', '--diff-filter=ACM', '--staged'])); - } - - /** Retrieve a list of all files tracked in the repository. */ - allFiles(): string[] { - 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. - */ - sanitizeConsoleOutput(value: string) { - return value; - } - - /** Whether verbose logging of Git actions should be used. */ - private static verboseLogging = false; - - /** The singleton instance of the unauthenticated `GitClient`. */ - private static _unauthenticatedInstance: GitClient; - - /** Set the verbose logging state of all git client instances. */ - static setVerboseLoggingState(verbose: boolean) { - GitClient.verboseLogging = verbose; - } - - /** - * Static method to get the singleton instance of the `GitClient`, creating it - * if it has not yet been created. - */ - static get(): GitClient { - if (!this._unauthenticatedInstance) { - GitClient._unauthenticatedInstance = new GitClient(); - } - return GitClient._unauthenticatedInstance; - } -} - -/** - * 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: SpawnSyncReturns): string[] { - return gitCommandResult.stdout.split('\n').map(x => x.trim()).filter(x => !!x); -} - -/** Determines the repository base directory from the current working directory. */ -function determineRepoBaseDirFromCwd() { - // TODO(devversion): Replace with common spawn sync utility once available. - const {stdout, stderr, status} = spawnSync( - 'git', ['rev-parse --show-toplevel'], {shell: true, stdio: 'pipe', encoding: 'utf8'}); - 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(); -} diff --git a/dev-infra/utils/git/github-urls.ts b/dev-infra/utils/git/github-urls.ts deleted file mode 100644 index bce1e08fbae7bd..00000000000000 --- a/dev-infra/utils/git/github-urls.ts +++ /dev/null @@ -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 - */ - - -import {URL} from 'url'; - -import {GithubConfig} from '../config'; -import {GitClient} from './git-client'; - -/** URL to the Github page where personal access tokens can be managed. */ -export const GITHUB_TOKEN_SETTINGS_URL = 'https://github.com/settings/tokens'; - -/** URL to the Github page where personal access tokens can be generated. */ -export const GITHUB_TOKEN_GENERATE_URL = 'https://github.com/settings/tokens/new'; - -/** Adds the provided token to the given Github HTTPs remote url. */ -export function addTokenToGitHttpsUrl(githubHttpsUrl: string, token: string) { - const url = new URL(githubHttpsUrl); - url.username = token; - return url.href; -} - -/** Gets the repository Git URL for the given github config. */ -export function getRepositoryGitUrl(config: GithubConfig, githubToken?: string): string { - if (config.useSsh) { - return `git@github.com:${config.owner}/${config.name}.git`; - } - const baseHttpUrl = `https://github.com/${config.owner}/${config.name}.git`; - if (githubToken !== undefined) { - return addTokenToGitHttpsUrl(baseHttpUrl, githubToken); - } - return baseHttpUrl; -} - -/** Gets a Github URL that refers to a list of recent commits within a specified branch. */ -export function getListCommitsInBranchUrl({remoteParams}: GitClient, branchName: string) { - return `https://github.com/${remoteParams.owner}/${remoteParams.repo}/commits/${branchName}`; -} diff --git a/dev-infra/utils/git/github-yargs.ts b/dev-infra/utils/git/github-yargs.ts deleted file mode 100644 index 606d66bdd055ac..00000000000000 --- a/dev-infra/utils/git/github-yargs.ts +++ /dev/null @@ -1,44 +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 {error, red, yellow} from '../console'; - -import {AuthenticatedGitClient} from './authenticated-git-client'; -import {GITHUB_TOKEN_GENERATE_URL} from './github-urls'; - -export type ArgvWithGithubToken = Argv<{githubToken: string}>; - -/** Sets up the `github-token` command option for the given Yargs instance. */ -export function addGithubTokenOption(yargs: Argv): ArgvWithGithubToken { - return yargs - // 'github-token' is casted to 'githubToken' to properly set up typings to reflect the key in - // the Argv object being camelCase rather than kebab case due to the `camel-case-expansion` - // config: https://github.com/yargs/yargs-parser#camel-case-expansion - .option('github-token' as 'githubToken', { - type: 'string', - description: 'Github token. If not set, token is retrieved from the environment variables.', - coerce: (token: string) => { - const githubToken = token || process.env.GITHUB_TOKEN || process.env.TOKEN; - if (!githubToken) { - error(red('No Github token set. Please set the `GITHUB_TOKEN` environment variable.')); - error(red('Alternatively, pass the `--github-token` command line flag.')); - error(yellow(`You can generate a token here: ${GITHUB_TOKEN_GENERATE_URL}`)); - process.exit(1); - } - try { - AuthenticatedGitClient.get(); - } catch { - AuthenticatedGitClient.configure(githubToken); - } - return githubToken; - }, - }) - .default('github-token' as 'githubToken', '', ''); -} diff --git a/dev-infra/utils/git/github.ts b/dev-infra/utils/git/github.ts deleted file mode 100644 index 3b0891d1c70d89..00000000000000 --- a/dev-infra/utils/git/github.ts +++ /dev/null @@ -1,75 +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 {OctokitOptions} from '@octokit/core/dist-types/types'; -import {graphql} from '@octokit/graphql'; -import {PaginateInterface} from '@octokit/plugin-paginate-rest'; -import {RestEndpointMethods} from '@octokit/plugin-rest-endpoint-methods/dist-types/generated/method-types'; -import {Octokit} from '@octokit/rest'; -import {RequestParameters} from '@octokit/types'; -import {query} from 'typed-graphqlify'; - -/** - * An object representation of a Graphql Query to be used as a response type and - * to generate a Graphql query string. - */ -export type GraphqlQueryObject = Parameters[1]; - -/** Interface describing a Github repository. */ -export interface GithubRepo { - /** Owner login of the repository. */ - owner: string; - /** Name of the repository. */ - name: string; -} - -/** Error for failed Github API requests. */ -export class GithubApiRequestError extends Error { - constructor(public status: number, message: string) { - super(message); - } -} - -/** A Github client for interacting with the Github APIs. */ -export class GithubClient { - /** The octokit instance actually performing API requests. */ - private _octokit = new Octokit(this._octokitOptions); - - readonly pulls = this._octokit.pulls; - readonly repos = this._octokit.repos; - readonly issues = this._octokit.issues; - readonly git = this._octokit.git; - readonly rateLimit = this._octokit.rateLimit; - readonly 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. - readonly rest: RestEndpointMethods = this._octokit.rest; - readonly paginate: PaginateInterface = this._octokit.paginate; - - constructor(private _octokitOptions?: OctokitOptions) {} -} - -/** - * Extension of the `GithubClient` that provides utilities which are specific - * to authenticated instances. - */ -export class AuthenticatedGithubClient extends GithubClient { - /** The graphql instance with authentication set during construction. */ - private _graphql = graphql.defaults({headers: {authorization: `token ${this._token}`}}); - - constructor(private _token: string) { - // Set the token for the octokit instance. - super({auth: _token}); - } - - /** Perform a query using Github's Graphql API. */ - async graphql(queryObject: T, params: RequestParameters = {}) { - return (await this._graphql(query(queryObject).toString(), params)) as T; - } -} diff --git a/dev-infra/utils/github.ts b/dev-infra/utils/github.ts deleted file mode 100644 index 9df744378ed47d..00000000000000 --- a/dev-infra/utils/github.ts +++ /dev/null @@ -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 {params, types} from 'typed-graphqlify'; -import {AuthenticatedGitClient} from './git/authenticated-git-client'; - - -/** Get a PR from github */ -export async function getPr( - prSchema: PrSchema, prNumber: number, git: AuthenticatedGitClient) { - /** The owner and name of the repository */ - const {owner, name} = git.remoteConfig; - /** The Graphql query object to get a the PR */ - const PR_QUERY = params( - { - $number: 'Int!', // The PR number - $owner: 'String!', // The organization to query for - $name: 'String!', // The organization to query for - }, - { - repository: params({owner: '$owner', name: '$name'}, { - pullRequest: params({number: '$number'}, prSchema), - }) - }); - - const result = (await git.github.graphql(PR_QUERY, {number: prNumber, owner, name})); - return result.repository.pullRequest; -} - -/** Get all pending PRs from github */ -export async function getPendingPrs(prSchema: PrSchema, git: AuthenticatedGitClient) { - /** The owner and name of the repository */ - const {owner, name} = git.remoteConfig; - /** The Graphql query object to get a page of pending PRs */ - const PRS_QUERY = params( - { - $first: 'Int', // How many entries to get with each request - $after: 'String', // The cursor to start the page at - $owner: 'String!', // The organization to query for - $name: 'String!', // The repository to query for - }, - { - repository: params({owner: '$owner', name: '$name'}, { - pullRequests: params( - { - first: '$first', - after: '$after', - states: `OPEN`, - }, - { - nodes: [prSchema], - pageInfo: { - hasNextPage: types.boolean, - endCursor: types.string, - }, - }), - }) - }); - /** The current cursor */ - let cursor: string|undefined; - /** If an additional page of members is expected */ - let hasNextPage = true; - /** Array of pending PRs */ - const prs: Array = []; - - // For each page of the response, get the page and add it to the list of PRs - while (hasNextPage) { - const params = { - after: cursor || null, - first: 100, - owner, - name, - }; - const results = await git.github.graphql(PRS_QUERY, params) as typeof PRS_QUERY; - prs.push(...results.repository.pullRequests.nodes); - hasNextPage = results.repository.pullRequests.pageInfo.hasNextPage; - cursor = results.repository.pullRequests.pageInfo.endCursor; - } - return prs; -} diff --git a/dev-infra/utils/inquirer-autocomplete-typings.d.ts b/dev-infra/utils/inquirer-autocomplete-typings.d.ts deleted file mode 100644 index bddf9e2e1a20cc..00000000000000 --- a/dev-infra/utils/inquirer-autocomplete-typings.d.ts +++ /dev/null @@ -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 - */ - -// inquirer-autocomplete-prompt doesn't provide types and no types are made available via -// DefinitelyTyped. -declare module "inquirer-autocomplete-prompt" { - - import {registerPrompt} from 'inquirer'; - - let AutocompletePrompt: Parameters[1]; - export = AutocompletePrompt; -} diff --git a/dev-infra/utils/semver.ts b/dev-infra/utils/semver.ts deleted file mode 100644 index ad2141435ff311..00000000000000 --- a/dev-infra/utils/semver.ts +++ /dev/null @@ -1,28 +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 semver from 'semver'; - -/** - * Increments a specified SemVer version. Compared to the original increment in SemVer, - * the version is cloned to not modify the original version instance. - */ -export function semverInc( - version: semver.SemVer, release: semver.ReleaseType, identifier?: string) { - const clone = new semver.SemVer(version.version); - return clone.inc(release, identifier); -} - -/** Creates the equivalent experimental version for a provided SemVer. */ -export function createExperimentalSemver(version: string|semver.SemVer): semver.SemVer { - version = new semver.SemVer(version); - const experimentalVersion = new semver.SemVer(version.format()); - experimentalVersion.major = 0; - experimentalVersion.minor = version.major * 100 + version.minor; - return new semver.SemVer(experimentalVersion.format()); -} diff --git a/dev-infra/utils/testing/BUILD.bazel b/dev-infra/utils/testing/BUILD.bazel deleted file mode 100644 index 160d4391971ae6..00000000000000 --- a/dev-infra/utils/testing/BUILD.bazel +++ /dev/null @@ -1,16 +0,0 @@ -load("//dev-infra:defaults.bzl", "ts_library") - -ts_library( - name = "testing", - srcs = glob(["*.ts"]), - visibility = ["//dev-infra:__subpackages__"], - deps = [ - "//dev-infra/utils", - "@npm//@types/jasmine", - "@npm//@types/minimist", - "@npm//@types/node", - "@npm//@types/semver", - "@npm//minimist", - "@npm//semver", - ], -) diff --git a/dev-infra/utils/testing/github-pagination-header.ts b/dev-infra/utils/testing/github-pagination-header.ts deleted file mode 100644 index 094250cbf50f95..00000000000000 --- a/dev-infra/utils/testing/github-pagination-header.ts +++ /dev/null @@ -1,28 +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 - */ - -/** - * Builds the `Link` response header to indicate an API response that is suitable - * for pagination. This follows the specification as outlined within: - * https://docs.github.com/en/rest/guides/traversing-with-pagination - */ -export function buildGithubPaginationResponseHeader( - totalPages: number, currentPage: number, baseUrl: string) { - const links = [`<${baseUrl}?page=1>; rel="first"`, `<${baseUrl}?page=${totalPages}>; rel="last"`]; - - if (currentPage < totalPages) { - links.push(`<${baseUrl}?page=${currentPage + 1}>; rel="next"`); - } - - // Pages start with `1` as per the Github API specification. - if (currentPage > 1) { - links.push(`<${baseUrl}?page=${currentPage - 1}>; rel="prev"`); - } - - return links.join(','); -} diff --git a/dev-infra/utils/testing/index.ts b/dev-infra/utils/testing/index.ts deleted file mode 100644 index 30052d6e230d46..00000000000000 --- a/dev-infra/utils/testing/index.ts +++ /dev/null @@ -1,11 +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 * from './semver-matchers'; -export * from './virtual-git-client'; -export * from './virtual-git-matchers'; diff --git a/dev-infra/utils/testing/semver-matchers.ts b/dev-infra/utils/testing/semver-matchers.ts deleted file mode 100644 index fa38e6af12bc41..00000000000000 --- a/dev-infra/utils/testing/semver-matchers.ts +++ /dev/null @@ -1,12 +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 - */ - -/** Gets a jasmine asymmetric matcher for matching a given SemVer version. */ -export function matchesVersion(versionName: string) { - return jasmine.objectContaining({raw: versionName, version: versionName}); -} diff --git a/dev-infra/utils/testing/virtual-git-client.ts b/dev-infra/utils/testing/virtual-git-client.ts deleted file mode 100644 index 17202127c2e233..00000000000000 --- a/dev-infra/utils/testing/virtual-git-client.ts +++ /dev/null @@ -1,217 +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 {SpawnSyncOptions, SpawnSyncReturns} from 'child_process'; -import * as parseArgs from 'minimist'; -import {SemVer} from 'semver'; - -import {NgDevConfig} from '../config'; -import {AuthenticatedGitClient} from '../git/authenticated-git-client'; -import {GitClient} from '../git/git-client'; - -/** - * Temporary directory which will be used as project directory in tests. Note that - * this environment variable is automatically set by Bazel for tests. - */ -export const testTmpDir: string = process.env['TEST_TMPDIR']!; - - -/** A mock instance of a configuration for the ng-dev toolset for default testing. */ -export const mockNgDevConfig: NgDevConfig = { - github: { - name: 'name', - owner: 'owner', - } -}; - - -/** Type describing a Git head. */ -interface GitHead { - /** Name of the head. Not defined in a detached state. */ - branch?: string; - /** Ref associated with this head. i.e. the remote base of this head. */ - ref?: RemoteRef; - /** List of commits added to this head (on top of the ref's base). */ - newCommits: Commit[]; -} - -/** Type describing a remote Git ref. */ -export interface RemoteRef { - /** Name of the reference. */ - name: string; - /** Repository containing this ref. */ - repoUrl: string; -} - -/** Type describing a Git commit. */ -export interface Commit { - /** Commit message. */ - message: string; - /** List of files included in this commit. */ - files: string[]; -} - -/** - * Virtual git client that mocks Git commands and keeps track of the repository state - * in memory. This allows for convenient test assertions with Git interactions. - */ -export class VirtualGitClient extends AuthenticatedGitClient { - static createInstance(config = mockNgDevConfig, tmpDir = testTmpDir): VirtualGitClient { - return new VirtualGitClient('abc123', tmpDir, config); - } - - /** Current Git HEAD that has been previously fetched. */ - fetchHeadRef: RemoteRef|null = null; - /** List of known branches in the repository. */ - branches: {[branchName: string]: GitHead} = {master: {branch: 'master', newCommits: []}}; - /** Current checked out HEAD in the repository. */ - head: GitHead = this.branches['master']; - /** List of pushed heads to a given remote ref. */ - pushed: {remote: RemoteRef, head: GitHead}[] = []; - - /** - * Override the actual GitClient getLatestSemverTag, as an actual tag cannot be retrieved in - * testing. - */ - override getLatestSemverTag() { - return new SemVer('0.0.0'); - } - - /** - * Override the actual GitClient getLatestSemverTag, as an actual tags cannot be checked during - * testing, return back the SemVer version as the tag. - */ - override getMatchingTagForSemver(semver: SemVer) { - return semver.format(); - } - - /** Override for the actual Git client command execution. */ - override runGraceful(args: string[], options: SpawnSyncOptions = {}): SpawnSyncReturns { - const [command, ...rawArgs] = args; - switch (command) { - case 'push': - this._push(rawArgs); - break; - case 'fetch': - this._fetch(rawArgs); - break; - case 'checkout': - this._checkout(rawArgs); - break; - case 'commit': - this._commit(rawArgs); - break; - } - - // Return a fake spawn sync return value. We error non-gracefully if any command fails - // in the tests, so we always return success and stub out the `SpawnSyncReturns` type. - return {status: 0, stderr: '', output: [], pid: -1, signal: null, stdout: ''}; - } - - /** Handler for the `git push` command. */ - private _push(args: string[]) { - const [repoUrl, refspec] = parseArgs(args, {boolean: ['q']})._; - const ref = this._unwrapRefspec(refspec); - const name = ref.destination || ref.source; - const existingPush = - this.pushed.find(({remote}) => remote.repoUrl === repoUrl && remote.name === name); - const pushedHead = this._cloneHead(this.head); - - // Either, update a previously pushed branch, or keep track of a newly - // performed branch push. We don't respect the `--force` flag. - if (existingPush !== undefined) { - existingPush.head = pushedHead; - } else { - this.pushed.push({remote: {repoUrl, name}, head: pushedHead}); - } - } - - /** Handler for the `git commit` command. */ - private _commit(rawArgs: string[]) { - const args = parseArgs(rawArgs, {string: ['m', 'message']}); - const message = args['m'] || args['message']; - const files = args._; - if (!message) { - throw Error('No commit message has been specified.'); - } - this.head.newCommits.push({message, files}); - } - - /** Handler for the `git fetch` command. */ - private _fetch(rawArgs: string[]) { - const args = parseArgs(rawArgs, {boolean: ['f', 'force', 'q', 'quiet']}); - const [repoUrl, refspec] = args._; - const force = args['f'] || args['force']; - const ref = this._unwrapRefspec(refspec); - - // Keep track of the fetch head, so that it can be checked out - // later in a detached state. - this.fetchHeadRef = {name: ref.source, repoUrl}; - - // If a destination has been specified in the ref spec, add it to the - // list of available local branches. - if (ref.destination) { - if (this.branches[ref.destination] && !force) { - throw Error('Cannot override existing local branch when fetching.'); - } - this.branches[ref.destination] = { - branch: ref.destination, - ref: this.fetchHeadRef, - newCommits: [], - }; - } - } - - /** Handler for the `git checkout` command. */ - private _checkout(rawArgs: string[]) { - const args = parseArgs(rawArgs, {boolean: ['detach', 'B', 'q']}); - const createBranch = args['B']; - const detached = args['detach']; - const [target] = args._; - - if (target === 'FETCH_HEAD') { - if (this.fetchHeadRef === null) { - throw Error('Unexpectedly trying to check out "FETCH_HEAD". Not fetch head set.'); - } - this.head = {ref: this.fetchHeadRef, newCommits: []}; - } else if (this.branches[target]) { - this.head = this._cloneHead(this.branches[target], detached); - } else if (createBranch) { - this.head = this.branches[target] = {branch: target, ...this._cloneHead(this.head, detached)}; - } else { - throw Error(`Unexpected branch checked out: ${target}`); - } - } - - /** - * Unwraps a refspec into the base and target ref names. - * https://git-scm.com/docs/git-fetch#Documentation/git-fetch.txt-ltrefspecgt. - */ - private _unwrapRefspec(refspec: string): {source: string, destination?: string} { - const [source, destination] = refspec.split(':'); - if (!destination) { - return {source}; - } else { - return {source, destination}; - } - } - - /** Clones the specified Git head with respect to the detached flag. */ - private _cloneHead(head: GitHead, detached = false): GitHead { - return { - branch: detached ? undefined : head.branch, - ref: head.ref, - newCommits: [...head.newCommits], - }; - } -} - -export function installVirtualGitClientSpies(mockInstance = VirtualGitClient.createInstance()) { - spyOn(GitClient, 'get').and.returnValue(mockInstance); - spyOn(AuthenticatedGitClient, 'get').and.returnValue(mockInstance); -} diff --git a/dev-infra/utils/testing/virtual-git-matchers.ts b/dev-infra/utils/testing/virtual-git-matchers.ts deleted file mode 100644 index 6e09d76facc53d..00000000000000 --- a/dev-infra/utils/testing/virtual-git-matchers.ts +++ /dev/null @@ -1,41 +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 {GithubRepo} from '../git/github'; - -import {Commit} from './virtual-git-client'; - -/** Interface describing the match parameters for a virtual Git client push. */ -interface BranchPushMatchParameters { - targetRepo: GithubRepo; - targetBranch: string; - baseRepo: GithubRepo; - baseBranch: string; - expectedCommits: Commit[]|jasmine.ArrayContaining; -} - -/** - * Gets a jasmine object matcher for asserting that a virtual Git client push - * matches the specified branch push (through the match parameters). - */ -export function getBranchPushMatcher(options: BranchPushMatchParameters) { - const {targetRepo, targetBranch, baseBranch, baseRepo, expectedCommits} = options; - return jasmine.objectContaining({ - remote: { - repoUrl: `https://abc123@github.com/${targetRepo.owner}/${targetRepo.name}.git`, - name: `refs/heads/${targetBranch}` - }, - head: jasmine.objectContaining({ - newCommits: expectedCommits, - ref: { - repoUrl: `https://abc123@github.com/${baseRepo.owner}/${baseRepo.name}.git`, - name: baseBranch, - }, - }) - }); -} diff --git a/dev-infra/utils/ts-node.ts b/dev-infra/utils/ts-node.ts deleted file mode 100644 index f8bb14079435eb..00000000000000 --- a/dev-infra/utils/ts-node.ts +++ /dev/null @@ -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 - */ - -/** Whether ts-node has been installed and is available to ng-dev. */ -export function isTsNodeAvailable(): boolean { - try { - require.resolve('ts-node'); - return true; - } catch { - return false; - } -} diff --git a/docs/FIXUP_COMMITS.md b/docs/FIXUP_COMMITS.md index c32a91ef3d708b..385c36115fc9d5 100644 --- a/docs/FIXUP_COMMITS.md +++ b/docs/FIXUP_COMMITS.md @@ -48,7 +48,7 @@ Here is where fixup commits come in handy. By addressing review feedback in fixup commits, you make it very straight forward for the reviewer to see what are the new changes that need to be reviewed and verify that their earlier feedback has been addressed. This can save a lot of effort, especially on larger Pull Requests (where having to re-review _all_ the changes is pretty wasteful). -When the time comes to merge the Pull Request into the repository, the merge script [knows how to automatically squash](../dev-infra/pr/merge/strategies/autosquash-merge.ts) fixup commits with the corresponding regular commits. +When the time comes to merge the Pull Request into the repository, the merge script knows how to automatically squash fixup commits with the corresponding regular commits. ## Creating fixup commits @@ -69,7 +69,7 @@ git commit --fixup ... ## Squashing fixup commits -As mentioned above, the merge script will [automatically squash](../dev-infra/pr/merge/strategies/autosquash-merge.ts) fixup commits. +As mentioned above, the merge script will automatically squash fixup commits. However, sometimes you might want to manually squash a fixup commit. diff --git a/modules/benchmarks/e2e_test.bzl b/modules/benchmarks/e2e_test.bzl index bbcad88335af75..0a6a308638bdd9 100644 --- a/modules/benchmarks/e2e_test.bzl +++ b/modules/benchmarks/e2e_test.bzl @@ -9,7 +9,7 @@ load("//tools:defaults.bzl", "protractor_web_test_suite") def e2e_test(name, server, **kwargs): protractor_web_test_suite( name = name, - on_prepare = "//dev-infra/benchmark/component_benchmark:start-server.js", + on_prepare = "@npm//@angular/dev-infra-private/bazel/benchmark/component_benchmark:start-server.js", server = server, **kwargs ) diff --git a/modules/benchmarks/src/change_detection/BUILD.bazel b/modules/benchmarks/src/change_detection/BUILD.bazel index 4b2449f7f96bbc..a98c034829cc68 100644 --- a/modules/benchmarks/src/change_detection/BUILD.bazel +++ b/modules/benchmarks/src/change_detection/BUILD.bazel @@ -15,7 +15,7 @@ ts_library( srcs = ["change_detection.perf-spec.ts"], tsconfig = "//modules/benchmarks:tsconfig-e2e.json", deps = [ - "//dev-infra/benchmark/driver-utilities", + "@npm//@angular/dev-infra-private/bazel/benchmark/driver-utilities", "@npm//protractor", ], ) @@ -26,7 +26,7 @@ ts_library( srcs = ["change_detection.e2e-spec.ts"], tsconfig = "//modules/benchmarks:tsconfig-e2e.json", deps = [ - "//dev-infra/benchmark/driver-utilities", + "@npm//@angular/dev-infra-private/bazel/benchmark/driver-utilities", "@npm//protractor", ], ) diff --git a/modules/benchmarks/src/change_detection/change_detection.e2e-spec.ts b/modules/benchmarks/src/change_detection/change_detection.e2e-spec.ts index b8e8a76bc66ead..789d51824bd92a 100644 --- a/modules/benchmarks/src/change_detection/change_detection.e2e-spec.ts +++ b/modules/benchmarks/src/change_detection/change_detection.e2e-spec.ts @@ -6,10 +6,9 @@ * found in the LICENSE file at https://angular.io/license */ +import {openBrowser, verifyNoBrowserErrors} from '@angular/dev-infra-private/bazel/benchmark/driver-utilities'; import {$} from 'protractor'; -import {openBrowser, verifyNoBrowserErrors} from '../../../../dev-infra/benchmark/driver-utilities'; - describe('change detection benchmark', () => { afterEach(verifyNoBrowserErrors); diff --git a/modules/benchmarks/src/change_detection/change_detection.perf-spec.ts b/modules/benchmarks/src/change_detection/change_detection.perf-spec.ts index b4de37b70cc896..5c1491ac719254 100644 --- a/modules/benchmarks/src/change_detection/change_detection.perf-spec.ts +++ b/modules/benchmarks/src/change_detection/change_detection.perf-spec.ts @@ -6,8 +6,8 @@ * found in the LICENSE file at https://angular.io/license */ +import {runBenchmark, verifyNoBrowserErrors} from '@angular/dev-infra-private/bazel/benchmark/driver-utilities'; import {$} from 'protractor'; -import {runBenchmark, verifyNoBrowserErrors} from '../../../../dev-infra/benchmark/driver-utilities'; interface Worker { id: string; diff --git a/modules/benchmarks/src/change_detection/transplanted_views/BUILD.bazel b/modules/benchmarks/src/change_detection/transplanted_views/BUILD.bazel index 9b46290126dc01..40cdfb5d00cbc1 100644 --- a/modules/benchmarks/src/change_detection/transplanted_views/BUILD.bazel +++ b/modules/benchmarks/src/change_detection/transplanted_views/BUILD.bazel @@ -1,5 +1,5 @@ load("//tools:defaults.bzl", "ng_module", "ng_rollup_bundle", "ts_devserver") -load("//dev-infra/benchmark/component_benchmark:benchmark_test.bzl", "benchmark_test") +load("@npm//@angular/dev-infra-private/bazel/benchmark/component_benchmark:benchmark_test.bzl", "benchmark_test") load("//modules/benchmarks:e2e_test.bzl", "e2e_test") package(default_visibility = ["//modules/benchmarks:__subpackages__"]) diff --git a/modules/benchmarks/src/class_bindings/BUILD.bazel b/modules/benchmarks/src/class_bindings/BUILD.bazel index 8d727306bd9560..fa1d612b45bef4 100644 --- a/modules/benchmarks/src/class_bindings/BUILD.bazel +++ b/modules/benchmarks/src/class_bindings/BUILD.bazel @@ -1,4 +1,4 @@ -load("//dev-infra/benchmark/component_benchmark:component_benchmark.bzl", "component_benchmark") +load("@npm//@angular/dev-infra-private/bazel/benchmark/component_benchmark:component_benchmark.bzl", "component_benchmark") package(default_visibility = ["//modules/benchmarks:__subpackages__"]) @@ -6,7 +6,7 @@ component_benchmark( name = "benchmark", driver = ":class_bindings.perf-spec.ts", driver_deps = [ - "//dev-infra/benchmark/driver-utilities", + "@npm//@angular/dev-infra-private/bazel/benchmark/driver-utilities", "@npm//@types/jasmine", "@npm//protractor", ], diff --git a/modules/benchmarks/src/class_bindings/class_bindings.perf-spec.ts b/modules/benchmarks/src/class_bindings/class_bindings.perf-spec.ts index 39814c2af3115d..0fac5c7cdb885e 100644 --- a/modules/benchmarks/src/class_bindings/class_bindings.perf-spec.ts +++ b/modules/benchmarks/src/class_bindings/class_bindings.perf-spec.ts @@ -6,8 +6,8 @@ * found in the LICENSE file at https://angular.io/license */ +import {runBenchmark} from '@angular/dev-infra-private/bazel/benchmark/driver-utilities'; import {$, browser} from 'protractor'; -import {runBenchmark} from '../../../../dev-infra/benchmark/driver-utilities'; describe('class bindings perf', () => { it('should work for update', async () => { diff --git a/modules/benchmarks/src/expanding_rows/BUILD.bazel b/modules/benchmarks/src/expanding_rows/BUILD.bazel index 05aef08aebf38e..ce611bc602cffd 100644 --- a/modules/benchmarks/src/expanding_rows/BUILD.bazel +++ b/modules/benchmarks/src/expanding_rows/BUILD.bazel @@ -1,7 +1,7 @@ -package(default_visibility = ["//modules/benchmarks:__subpackages__"]) - load("//tools:defaults.bzl", "ng_module", "ng_rollup_bundle", "ts_devserver", "ts_library") -load("//dev-infra/benchmark/component_benchmark:benchmark_test.bzl", "benchmark_test") +load("@npm//@angular/dev-infra-private/bazel/benchmark/component_benchmark:benchmark_test.bzl", "benchmark_test") + +package(default_visibility = ["//modules/benchmarks:__subpackages__"]) ng_module( name = "application_lib", @@ -26,7 +26,7 @@ ts_library( srcs = ["expanding_rows.perf-spec.ts"], tsconfig = "//modules/benchmarks:tsconfig-e2e.json", deps = [ - "//dev-infra/benchmark/driver-utilities", + "@npm//@angular/dev-infra-private/bazel/benchmark/driver-utilities", "@npm//protractor", ], ) diff --git a/modules/benchmarks/src/expanding_rows/expanding_rows.perf-spec.ts b/modules/benchmarks/src/expanding_rows/expanding_rows.perf-spec.ts index 9691903b0afb9f..976b7b1954413f 100644 --- a/modules/benchmarks/src/expanding_rows/expanding_rows.perf-spec.ts +++ b/modules/benchmarks/src/expanding_rows/expanding_rows.perf-spec.ts @@ -6,8 +6,8 @@ * found in the LICENSE file at https://angular.io/license */ +import {runBenchmark} from '@angular/dev-infra-private/bazel/benchmark/driver-utilities'; import {$, browser} from 'protractor'; -import {runBenchmark} from '../../../../dev-infra/benchmark/driver-utilities'; describe('benchmarks', () => { it('should work for create', async () => { diff --git a/modules/benchmarks/src/js-web-frameworks/BUILD.bazel b/modules/benchmarks/src/js-web-frameworks/BUILD.bazel index dedd600ee50cca..f545b40e3e64f7 100644 --- a/modules/benchmarks/src/js-web-frameworks/BUILD.bazel +++ b/modules/benchmarks/src/js-web-frameworks/BUILD.bazel @@ -8,7 +8,7 @@ ts_library( srcs = ["js-web-frameworks.perf-spec.ts"], tsconfig = "//modules/benchmarks:tsconfig-e2e.json", deps = [ - "//dev-infra/benchmark/driver-utilities", + "@npm//@angular/dev-infra-private/bazel/benchmark/driver-utilities", "@npm//protractor", ], ) diff --git a/modules/benchmarks/src/js-web-frameworks/js-web-frameworks.perf-spec.ts b/modules/benchmarks/src/js-web-frameworks/js-web-frameworks.perf-spec.ts index c70820fafad219..d18646c0be44a2 100644 --- a/modules/benchmarks/src/js-web-frameworks/js-web-frameworks.perf-spec.ts +++ b/modules/benchmarks/src/js-web-frameworks/js-web-frameworks.perf-spec.ts @@ -6,8 +6,8 @@ * found in the LICENSE file at https://angular.io/license */ +import {runBenchmark, verifyNoBrowserErrors} from '@angular/dev-infra-private/bazel/benchmark/driver-utilities'; import {$} from 'protractor'; -import {runBenchmark, verifyNoBrowserErrors} from '../../../../dev-infra/benchmark/driver-utilities'; interface Worker { id: string; diff --git a/modules/benchmarks/src/js-web-frameworks/ng2/BUILD.bazel b/modules/benchmarks/src/js-web-frameworks/ng2/BUILD.bazel index df8694d3bf0aec..5692c34ad6cb1a 100644 --- a/modules/benchmarks/src/js-web-frameworks/ng2/BUILD.bazel +++ b/modules/benchmarks/src/js-web-frameworks/ng2/BUILD.bazel @@ -1,5 +1,5 @@ load("//tools:defaults.bzl", "ng_module", "ng_rollup_bundle", "ts_devserver") -load("//dev-infra/benchmark/component_benchmark:benchmark_test.bzl", "benchmark_test") +load("@npm//@angular/dev-infra-private/bazel/benchmark/component_benchmark:benchmark_test.bzl", "benchmark_test") package(default_visibility = ["//modules/benchmarks:__subpackages__"]) diff --git a/modules/benchmarks/src/largeform/BUILD.bazel b/modules/benchmarks/src/largeform/BUILD.bazel index 81273b7146c98d..80e5a42d1656f7 100644 --- a/modules/benchmarks/src/largeform/BUILD.bazel +++ b/modules/benchmarks/src/largeform/BUILD.bazel @@ -1,14 +1,14 @@ -package(default_visibility = ["//modules/benchmarks:__subpackages__"]) - load("//tools:defaults.bzl", "ts_library") +package(default_visibility = ["//modules/benchmarks:__subpackages__"]) + ts_library( name = "perf_tests_lib", testonly = 1, srcs = ["largeform.perf-spec.ts"], tsconfig = "//modules/benchmarks:tsconfig-e2e.json", deps = [ - "//dev-infra/benchmark/driver-utilities", + "@npm//@angular/dev-infra-private/bazel/benchmark/driver-utilities", "@npm//protractor", ], ) @@ -19,7 +19,7 @@ ts_library( srcs = ["largeform.e2e-spec.ts"], tsconfig = "//modules/benchmarks:tsconfig-e2e.json", deps = [ - "//dev-infra/benchmark/driver-utilities", + "@npm//@angular/dev-infra-private/bazel/benchmark/driver-utilities", "@npm//protractor", ], ) diff --git a/modules/benchmarks/src/largeform/largeform.e2e-spec.ts b/modules/benchmarks/src/largeform/largeform.e2e-spec.ts index 8237955cc8c3dd..2c72b1c6967ad0 100644 --- a/modules/benchmarks/src/largeform/largeform.e2e-spec.ts +++ b/modules/benchmarks/src/largeform/largeform.e2e-spec.ts @@ -6,10 +6,9 @@ * found in the LICENSE file at https://angular.io/license */ +import {openBrowser, verifyNoBrowserErrors} from '@angular/dev-infra-private/bazel/benchmark/driver-utilities'; import {$, By, element} from 'protractor'; -import {openBrowser, verifyNoBrowserErrors} from '../../../../dev-infra/benchmark/driver-utilities'; - describe('largeform benchmark', () => { afterEach(verifyNoBrowserErrors); diff --git a/modules/benchmarks/src/largeform/largeform.perf-spec.ts b/modules/benchmarks/src/largeform/largeform.perf-spec.ts index aba123cf48e311..bbb26ce0a5b4bf 100644 --- a/modules/benchmarks/src/largeform/largeform.perf-spec.ts +++ b/modules/benchmarks/src/largeform/largeform.perf-spec.ts @@ -6,10 +6,9 @@ * found in the LICENSE file at https://angular.io/license */ +import {runBenchmark, verifyNoBrowserErrors} from '@angular/dev-infra-private/bazel/benchmark/driver-utilities'; import {$} from 'protractor'; -import {runBenchmark, verifyNoBrowserErrors} from '../../../../dev-infra/benchmark/driver-utilities'; - interface Worker { id: string; prepare?(): void; diff --git a/modules/benchmarks/src/largeform/ng2/BUILD.bazel b/modules/benchmarks/src/largeform/ng2/BUILD.bazel index 7fd2e3761913ec..871de42f91e049 100644 --- a/modules/benchmarks/src/largeform/ng2/BUILD.bazel +++ b/modules/benchmarks/src/largeform/ng2/BUILD.bazel @@ -1,5 +1,5 @@ load("//tools:defaults.bzl", "ng_module", "ts_devserver") -load("//dev-infra/benchmark/component_benchmark:benchmark_test.bzl", "benchmark_test") +load("@npm//@angular/dev-infra-private/bazel/benchmark/component_benchmark:benchmark_test.bzl", "benchmark_test") load("//modules/benchmarks:e2e_test.bzl", "e2e_test") package(default_visibility = ["//modules/benchmarks:__subpackages__"]) diff --git a/modules/benchmarks/src/largetable/BUILD.bazel b/modules/benchmarks/src/largetable/BUILD.bazel index 68bc416fe25c25..2b850d7719b8f9 100644 --- a/modules/benchmarks/src/largetable/BUILD.bazel +++ b/modules/benchmarks/src/largetable/BUILD.bazel @@ -15,7 +15,7 @@ ts_library( srcs = ["largetable.perf-spec.ts"], tsconfig = "//modules/benchmarks:tsconfig-e2e.json", deps = [ - "//dev-infra/benchmark/driver-utilities", + "@npm//@angular/dev-infra-private/bazel/benchmark/driver-utilities", "@npm//protractor", ], ) @@ -26,7 +26,7 @@ ts_library( srcs = ["largetable.e2e-spec.ts"], tsconfig = "//modules/benchmarks:tsconfig-e2e.json", deps = [ - "//dev-infra/benchmark/driver-utilities", + "@npm//@angular/dev-infra-private/bazel/benchmark/driver-utilities", "@npm//protractor", ], ) diff --git a/modules/benchmarks/src/largetable/baseline/BUILD.bazel b/modules/benchmarks/src/largetable/baseline/BUILD.bazel index 297ab4eabf609b..c90e3a0093d329 100644 --- a/modules/benchmarks/src/largetable/baseline/BUILD.bazel +++ b/modules/benchmarks/src/largetable/baseline/BUILD.bazel @@ -1,5 +1,5 @@ load("//tools:defaults.bzl", "ts_devserver", "ts_library") -load("//dev-infra/benchmark/component_benchmark:benchmark_test.bzl", "benchmark_test") +load("@npm//@angular/dev-infra-private/bazel/benchmark/component_benchmark:benchmark_test.bzl", "benchmark_test") load("//modules/benchmarks:e2e_test.bzl", "e2e_test") package(default_visibility = ["//modules/benchmarks:__subpackages__"]) diff --git a/modules/benchmarks/src/largetable/incremental_dom/BUILD.bazel b/modules/benchmarks/src/largetable/incremental_dom/BUILD.bazel index c37bf4e258916e..79b622ab590b06 100644 --- a/modules/benchmarks/src/largetable/incremental_dom/BUILD.bazel +++ b/modules/benchmarks/src/largetable/incremental_dom/BUILD.bazel @@ -1,5 +1,5 @@ load("//tools:defaults.bzl", "ts_devserver", "ts_library") -load("//dev-infra/benchmark/component_benchmark:benchmark_test.bzl", "benchmark_test") +load("@npm//@angular/dev-infra-private/bazel/benchmark/component_benchmark:benchmark_test.bzl", "benchmark_test") load("//modules/benchmarks:e2e_test.bzl", "e2e_test") package(default_visibility = ["//modules/benchmarks:__subpackages__"]) diff --git a/modules/benchmarks/src/largetable/iv/BUILD.bazel b/modules/benchmarks/src/largetable/iv/BUILD.bazel index 4ade284237497d..896252fb442baa 100644 --- a/modules/benchmarks/src/largetable/iv/BUILD.bazel +++ b/modules/benchmarks/src/largetable/iv/BUILD.bazel @@ -1,5 +1,5 @@ load("//tools:defaults.bzl", "ts_devserver") -load("//dev-infra/benchmark/component_benchmark:benchmark_test.bzl", "benchmark_test") +load("@npm//@angular/dev-infra-private/bazel/benchmark/component_benchmark:benchmark_test.bzl", "benchmark_test") load("//modules/benchmarks:e2e_test.bzl", "e2e_test") package(default_visibility = ["//modules/benchmarks:__subpackages__"]) diff --git a/modules/benchmarks/src/largetable/largetable.e2e-spec.ts b/modules/benchmarks/src/largetable/largetable.e2e-spec.ts index c96c9c57eee5ab..0ff96319c44e8e 100644 --- a/modules/benchmarks/src/largetable/largetable.e2e-spec.ts +++ b/modules/benchmarks/src/largetable/largetable.e2e-spec.ts @@ -6,10 +6,9 @@ * found in the LICENSE file at https://angular.io/license */ +import {openBrowser, verifyNoBrowserErrors} from '@angular/dev-infra-private/bazel/benchmark/driver-utilities'; import {$} from 'protractor'; -import {openBrowser, verifyNoBrowserErrors} from '../../../../dev-infra/benchmark/driver-utilities'; - describe('largetable benchmark', () => { afterEach(verifyNoBrowserErrors); diff --git a/modules/benchmarks/src/largetable/largetable.perf-spec.ts b/modules/benchmarks/src/largetable/largetable.perf-spec.ts index e57d669858aaa8..dbc1aa586887ca 100644 --- a/modules/benchmarks/src/largetable/largetable.perf-spec.ts +++ b/modules/benchmarks/src/largetable/largetable.perf-spec.ts @@ -6,8 +6,8 @@ * found in the LICENSE file at https://angular.io/license */ +import {runBenchmark, verifyNoBrowserErrors} from '@angular/dev-infra-private/bazel/benchmark/driver-utilities'; import {$} from 'protractor'; -import {runBenchmark, verifyNoBrowserErrors} from '../../../../dev-infra/benchmark/driver-utilities'; interface Worker { id: string; diff --git a/modules/benchmarks/src/largetable/ng2/BUILD.bazel b/modules/benchmarks/src/largetable/ng2/BUILD.bazel index e25e9c681bbe88..dec5af1b2f01ab 100644 --- a/modules/benchmarks/src/largetable/ng2/BUILD.bazel +++ b/modules/benchmarks/src/largetable/ng2/BUILD.bazel @@ -1,5 +1,5 @@ load("//tools:defaults.bzl", "ng_module", "ng_rollup_bundle", "ts_devserver") -load("//dev-infra/benchmark/component_benchmark:benchmark_test.bzl", "benchmark_test") +load("@npm//@angular/dev-infra-private/bazel/benchmark/component_benchmark:benchmark_test.bzl", "benchmark_test") load("//modules/benchmarks:e2e_test.bzl", "e2e_test") package(default_visibility = ["//modules/benchmarks:__subpackages__"]) diff --git a/modules/benchmarks/src/largetable/ng2_switch/BUILD.bazel b/modules/benchmarks/src/largetable/ng2_switch/BUILD.bazel index 18730643365284..e549635ac6a995 100644 --- a/modules/benchmarks/src/largetable/ng2_switch/BUILD.bazel +++ b/modules/benchmarks/src/largetable/ng2_switch/BUILD.bazel @@ -1,5 +1,5 @@ load("//tools:defaults.bzl", "ng_module", "ts_devserver") -load("//dev-infra/benchmark/component_benchmark:benchmark_test.bzl", "benchmark_test") +load("@npm//@angular/dev-infra-private/bazel/benchmark/component_benchmark:benchmark_test.bzl", "benchmark_test") load("//modules/benchmarks:e2e_test.bzl", "e2e_test") package(default_visibility = ["//modules/benchmarks:__subpackages__"]) diff --git a/modules/benchmarks/src/largetable/render3/BUILD.bazel b/modules/benchmarks/src/largetable/render3/BUILD.bazel index 54109654bb14f4..0c62a6f9301e81 100644 --- a/modules/benchmarks/src/largetable/render3/BUILD.bazel +++ b/modules/benchmarks/src/largetable/render3/BUILD.bazel @@ -1,9 +1,9 @@ -package(default_visibility = ["//visibility:public"]) - load("//tools:defaults.bzl", "ng_module", "ng_rollup_bundle", "ts_devserver") -load("//dev-infra/benchmark/component_benchmark:benchmark_test.bzl", "benchmark_test") +load("@npm//@angular/dev-infra-private/bazel/benchmark/component_benchmark:benchmark_test.bzl", "benchmark_test") load("//modules/benchmarks:e2e_test.bzl", "e2e_test") +package(default_visibility = ["//visibility:public"]) + ng_module( name = "largetable_lib", srcs = [ diff --git a/modules/benchmarks/src/styling/BUILD.bazel b/modules/benchmarks/src/styling/BUILD.bazel index a31bef834128ba..8213337c0f9d06 100644 --- a/modules/benchmarks/src/styling/BUILD.bazel +++ b/modules/benchmarks/src/styling/BUILD.bazel @@ -1,14 +1,14 @@ -package(default_visibility = ["//modules/benchmarks:__subpackages__"]) - load("//tools:defaults.bzl", "ts_library") +package(default_visibility = ["//modules/benchmarks:__subpackages__"]) + ts_library( name = "tests_lib", testonly = True, srcs = ["styling_perf.spec.ts"], tsconfig = "//modules/benchmarks:tsconfig-e2e.json", deps = [ - "//dev-infra/benchmark/driver-utilities", + "@npm//@angular/dev-infra-private/bazel/benchmark/driver-utilities", "@npm//protractor", ], ) diff --git a/modules/benchmarks/src/styling/ng2/BUILD.bazel b/modules/benchmarks/src/styling/ng2/BUILD.bazel index eb085d1745f7fc..773802f17df0d3 100644 --- a/modules/benchmarks/src/styling/ng2/BUILD.bazel +++ b/modules/benchmarks/src/styling/ng2/BUILD.bazel @@ -1,5 +1,5 @@ load("//tools:defaults.bzl", "ng_module", "ng_rollup_bundle", "ts_devserver") -load("//dev-infra/benchmark/component_benchmark:benchmark_test.bzl", "benchmark_test") +load("@npm//@angular/dev-infra-private/bazel/benchmark/component_benchmark:benchmark_test.bzl", "benchmark_test") package(default_visibility = ["//modules/benchmarks:__subpackages__"]) diff --git a/modules/benchmarks/src/styling/styling_perf.spec.ts b/modules/benchmarks/src/styling/styling_perf.spec.ts index 89a42eb94d16e9..511dd29416c752 100644 --- a/modules/benchmarks/src/styling/styling_perf.spec.ts +++ b/modules/benchmarks/src/styling/styling_perf.spec.ts @@ -6,8 +6,8 @@ * found in the LICENSE file at https://angular.io/license */ +import {openBrowser, runBenchmark, verifyNoBrowserErrors} from '@angular/dev-infra-private/bazel/benchmark/driver-utilities'; import {$, by, element} from 'protractor'; -import {openBrowser, runBenchmark, verifyNoBrowserErrors} from '../../../../dev-infra/benchmark/driver-utilities'; /** List of possible scenarios that should be tested. */ const SCENARIOS = [ diff --git a/modules/benchmarks/src/tree/BUILD.bazel b/modules/benchmarks/src/tree/BUILD.bazel index 7a7b4a59fc418a..e6da68d61de929 100644 --- a/modules/benchmarks/src/tree/BUILD.bazel +++ b/modules/benchmarks/src/tree/BUILD.bazel @@ -15,7 +15,7 @@ ts_library( srcs = ["test_utils.ts"], tsconfig = "//modules/benchmarks:tsconfig-e2e.json", deps = [ - "//dev-infra/benchmark/driver-utilities", + "@npm//@angular/dev-infra-private/bazel/benchmark/driver-utilities", "@npm//protractor", ], ) diff --git a/modules/benchmarks/src/tree/baseline/BUILD.bazel b/modules/benchmarks/src/tree/baseline/BUILD.bazel index 5392ffe227dcb7..23de50c79b31c1 100644 --- a/modules/benchmarks/src/tree/baseline/BUILD.bazel +++ b/modules/benchmarks/src/tree/baseline/BUILD.bazel @@ -1,5 +1,5 @@ load("//tools:defaults.bzl", "ts_devserver", "ts_library") -load("//dev-infra/benchmark/component_benchmark:benchmark_test.bzl", "benchmark_test") +load("@npm//@angular/dev-infra-private/bazel/benchmark/component_benchmark:benchmark_test.bzl", "benchmark_test") load("//modules/benchmarks:e2e_test.bzl", "e2e_test") package(default_visibility = ["//modules/benchmarks:__subpackages__"]) diff --git a/modules/benchmarks/src/tree/incremental_dom/BUILD.bazel b/modules/benchmarks/src/tree/incremental_dom/BUILD.bazel index 45ad6220e62899..723544557db642 100644 --- a/modules/benchmarks/src/tree/incremental_dom/BUILD.bazel +++ b/modules/benchmarks/src/tree/incremental_dom/BUILD.bazel @@ -1,5 +1,5 @@ load("//tools:defaults.bzl", "ts_devserver", "ts_library") -load("//dev-infra/benchmark/component_benchmark:benchmark_test.bzl", "benchmark_test") +load("@npm//@angular/dev-infra-private/bazel/benchmark/component_benchmark:benchmark_test.bzl", "benchmark_test") load("//modules/benchmarks:e2e_test.bzl", "e2e_test") package(default_visibility = ["//modules/benchmarks:__subpackages__"]) diff --git a/modules/benchmarks/src/tree/iv/BUILD.bazel b/modules/benchmarks/src/tree/iv/BUILD.bazel index fe253e0398ec54..45cabecd193f4a 100644 --- a/modules/benchmarks/src/tree/iv/BUILD.bazel +++ b/modules/benchmarks/src/tree/iv/BUILD.bazel @@ -1,5 +1,5 @@ load("//tools:defaults.bzl", "ts_devserver") -load("//dev-infra/benchmark/component_benchmark:benchmark_test.bzl", "benchmark_test") +load("@npm//@angular/dev-infra-private/bazel/benchmark/component_benchmark:benchmark_test.bzl", "benchmark_test") load("//modules/benchmarks:e2e_test.bzl", "e2e_test") package(default_visibility = ["//modules/benchmarks:__subpackages__"]) diff --git a/modules/benchmarks/src/tree/ng1/BUILD.bazel b/modules/benchmarks/src/tree/ng1/BUILD.bazel index 677f511ee00818..aecb68cf07e706 100644 --- a/modules/benchmarks/src/tree/ng1/BUILD.bazel +++ b/modules/benchmarks/src/tree/ng1/BUILD.bazel @@ -1,5 +1,5 @@ load("//tools:defaults.bzl", "ts_devserver", "ts_library") -load("//dev-infra/benchmark/component_benchmark:benchmark_test.bzl", "benchmark_test") +load("@npm//@angular/dev-infra-private/bazel/benchmark/component_benchmark:benchmark_test.bzl", "benchmark_test") load("//modules/benchmarks:e2e_test.bzl", "e2e_test") package(default_visibility = ["//modules/benchmarks:__subpackages__"]) diff --git a/modules/benchmarks/src/tree/ng2/BUILD.bazel b/modules/benchmarks/src/tree/ng2/BUILD.bazel index 74aab081bc80a5..82fa164aa64518 100644 --- a/modules/benchmarks/src/tree/ng2/BUILD.bazel +++ b/modules/benchmarks/src/tree/ng2/BUILD.bazel @@ -1,5 +1,5 @@ load("//tools:defaults.bzl", "ng_module", "ng_rollup_bundle", "ts_devserver") -load("//dev-infra/benchmark/component_benchmark:benchmark_test.bzl", "benchmark_test") +load("@npm//@angular/dev-infra-private/bazel/benchmark/component_benchmark:benchmark_test.bzl", "benchmark_test") load("//modules/benchmarks:e2e_test.bzl", "e2e_test") package(default_visibility = ["//modules/benchmarks:__subpackages__"]) diff --git a/modules/benchmarks/src/tree/ng2_next/BUILD.bazel b/modules/benchmarks/src/tree/ng2_next/BUILD.bazel index 54cbeaaf18c31c..001c8c49eeb9b3 100644 --- a/modules/benchmarks/src/tree/ng2_next/BUILD.bazel +++ b/modules/benchmarks/src/tree/ng2_next/BUILD.bazel @@ -1,5 +1,5 @@ load("//tools:defaults.bzl", "ts_devserver", "ts_library") -load("//dev-infra/benchmark/component_benchmark:benchmark_test.bzl", "benchmark_test") +load("@npm//@angular/dev-infra-private/bazel/benchmark/component_benchmark:benchmark_test.bzl", "benchmark_test") load("//modules/benchmarks:e2e_test.bzl", "e2e_test") package(default_visibility = ["//modules/benchmarks:__subpackages__"]) diff --git a/modules/benchmarks/src/tree/ng2_static/BUILD.bazel b/modules/benchmarks/src/tree/ng2_static/BUILD.bazel index 35fd394a35feca..077cbb8dd57370 100644 --- a/modules/benchmarks/src/tree/ng2_static/BUILD.bazel +++ b/modules/benchmarks/src/tree/ng2_static/BUILD.bazel @@ -1,5 +1,5 @@ load("//tools:defaults.bzl", "ts_devserver", "ts_library") -load("//dev-infra/benchmark/component_benchmark:benchmark_test.bzl", "benchmark_test") +load("@npm//@angular/dev-infra-private/bazel/benchmark/component_benchmark:benchmark_test.bzl", "benchmark_test") load("//modules/benchmarks:e2e_test.bzl", "e2e_test") package(default_visibility = ["//modules/benchmarks:__subpackages__"]) diff --git a/modules/benchmarks/src/tree/ng2_switch/BUILD.bazel b/modules/benchmarks/src/tree/ng2_switch/BUILD.bazel index 0dfa80e29abcc5..41e5c77db06c2c 100644 --- a/modules/benchmarks/src/tree/ng2_switch/BUILD.bazel +++ b/modules/benchmarks/src/tree/ng2_switch/BUILD.bazel @@ -1,5 +1,5 @@ load("//tools:defaults.bzl", "ng_module", "ts_devserver") -load("//dev-infra/benchmark/component_benchmark:benchmark_test.bzl", "benchmark_test") +load("@npm//@angular/dev-infra-private/bazel/benchmark/component_benchmark:benchmark_test.bzl", "benchmark_test") load("//modules/benchmarks:e2e_test.bzl", "e2e_test") package(default_visibility = ["//modules/benchmarks:__subpackages__"]) diff --git a/modules/benchmarks/src/tree/render3/BUILD.bazel b/modules/benchmarks/src/tree/render3/BUILD.bazel index c58f47baf44090..8c24d5c2440b8f 100644 --- a/modules/benchmarks/src/tree/render3/BUILD.bazel +++ b/modules/benchmarks/src/tree/render3/BUILD.bazel @@ -1,9 +1,9 @@ -package(default_visibility = ["//modules/benchmarks:__subpackages__"]) - load("//tools:defaults.bzl", "ng_module", "ng_rollup_bundle", "ts_devserver") -load("//dev-infra/benchmark/component_benchmark:benchmark_test.bzl", "benchmark_test") +load("@npm//@angular/dev-infra-private/bazel/benchmark/component_benchmark:benchmark_test.bzl", "benchmark_test") load("//modules/benchmarks:e2e_test.bzl", "e2e_test") +package(default_visibility = ["//modules/benchmarks:__subpackages__"]) + ng_module( name = "tree_lib", srcs = [ diff --git a/modules/benchmarks/src/tree/test_utils.ts b/modules/benchmarks/src/tree/test_utils.ts index cf6430c8009d4e..de9f2a88ef67d3 100644 --- a/modules/benchmarks/src/tree/test_utils.ts +++ b/modules/benchmarks/src/tree/test_utils.ts @@ -6,10 +6,9 @@ * found in the LICENSE file at https://angular.io/license */ +import {openBrowser, runBenchmark} from '@angular/dev-infra-private/bazel/benchmark/driver-utilities'; import {browser} from 'protractor'; -import {openBrowser, runBenchmark} from '../../../../dev-infra/benchmark/driver-utilities'; - export function runTreeBenchmark({id, prepare, setup, work}: { id: string; prepare ? () : void; setup ? () : void; work(): void; }) { diff --git a/modules/benchmarks/tsconfig.json b/modules/benchmarks/tsconfig.json index 02f26ff6e964ac..11f311cf2a08ea 100644 --- a/modules/benchmarks/tsconfig.json +++ b/modules/benchmarks/tsconfig.json @@ -31,5 +31,5 @@ "no-unused-expression": true, "no-unused-variable": true }, - "include": ["../../dev-infra/benchmark/driver-utilities/"] + "include": ["../../node_modules/@angular/dev-infra-private/bazel/benchmark/driver-utilities/"] } diff --git a/modules/playground/e2e_test/async/async_spec.ts b/modules/playground/e2e_test/async/async_spec.ts index 4c39c0e4a2698e..9a4c0c4d60489d 100644 --- a/modules/playground/e2e_test/async/async_spec.ts +++ b/modules/playground/e2e_test/async/async_spec.ts @@ -6,11 +6,10 @@ * found in the LICENSE file at https://angular.io/license */ +import {verifyNoBrowserErrors} from '@angular/dev-infra-private/bazel/benchmark/driver-utilities'; import {$, browser} from 'protractor'; import {promise} from 'selenium-webdriver'; -import {verifyNoBrowserErrors} from '../../../../dev-infra/benchmark/driver-utilities'; - describe('async', () => { const URL = '/'; diff --git a/modules/playground/e2e_test/example_test.bzl b/modules/playground/e2e_test/example_test.bzl index 64da4af375cc33..3070dc867a4060 100644 --- a/modules/playground/e2e_test/example_test.bzl +++ b/modules/playground/e2e_test/example_test.bzl @@ -7,7 +7,7 @@ def example_test(name, srcs, server, data = [], deps = [], **kwargs): srcs = srcs, tsconfig = "//modules/playground:tsconfig-e2e.json", deps = [ - "//dev-infra/benchmark/driver-utilities", + "@npm//@angular/dev-infra-private/bazel/benchmark/driver-utilities", "//packages/private/testing", "@npm//@types/jasminewd2", "@npm//@types/selenium-webdriver", diff --git a/modules/playground/e2e_test/hello_world/hello_world_spec.ts b/modules/playground/e2e_test/hello_world/hello_world_spec.ts index a9937cfb9ed0e7..c5e1284c386cca 100644 --- a/modules/playground/e2e_test/hello_world/hello_world_spec.ts +++ b/modules/playground/e2e_test/hello_world/hello_world_spec.ts @@ -6,10 +6,9 @@ * found in the LICENSE file at https://angular.io/license */ +import {verifyNoBrowserErrors} from '@angular/dev-infra-private/bazel/benchmark/driver-utilities'; import {browser} from 'protractor'; -import {verifyNoBrowserErrors} from '../../../../dev-infra/benchmark/driver-utilities'; - describe('hello world', function() { afterEach(verifyNoBrowserErrors); diff --git a/modules/playground/e2e_test/http/http_spec.ts b/modules/playground/e2e_test/http/http_spec.ts index d3ab33605ec832..8c2cdf76558508 100644 --- a/modules/playground/e2e_test/http/http_spec.ts +++ b/modules/playground/e2e_test/http/http_spec.ts @@ -6,10 +6,9 @@ * found in the LICENSE file at https://angular.io/license */ +import {verifyNoBrowserErrors} from '@angular/dev-infra-private/bazel/benchmark/driver-utilities'; import {browser} from 'protractor'; -import {verifyNoBrowserErrors} from '../../../../dev-infra/benchmark/driver-utilities'; - describe('http', function() { afterEach(verifyNoBrowserErrors); diff --git a/modules/playground/e2e_test/jsonp/jsonp_spec.ts b/modules/playground/e2e_test/jsonp/jsonp_spec.ts index 94cf17761976fc..9539380a54cd74 100644 --- a/modules/playground/e2e_test/jsonp/jsonp_spec.ts +++ b/modules/playground/e2e_test/jsonp/jsonp_spec.ts @@ -6,10 +6,9 @@ * found in the LICENSE file at https://angular.io/license */ +import {verifyNoBrowserErrors} from '@angular/dev-infra-private/bazel/benchmark/driver-utilities'; import {browser} from 'protractor'; -import {verifyNoBrowserErrors} from '../../../../dev-infra/benchmark/driver-utilities'; - describe('jsonp', function() { afterEach(verifyNoBrowserErrors); diff --git a/modules/playground/e2e_test/key_events/key_events_spec.ts b/modules/playground/e2e_test/key_events/key_events_spec.ts index ef6d08e80cbfbf..f42fbb6307a680 100644 --- a/modules/playground/e2e_test/key_events/key_events_spec.ts +++ b/modules/playground/e2e_test/key_events/key_events_spec.ts @@ -6,10 +6,9 @@ * found in the LICENSE file at https://angular.io/license */ +import {verifyNoBrowserErrors} from '@angular/dev-infra-private/bazel/benchmark/driver-utilities'; import {browser, by, element, protractor} from 'protractor'; -import {verifyNoBrowserErrors} from '../../../../dev-infra/benchmark/driver-utilities'; - const Key = protractor.Key; describe('key_events', function() { diff --git a/modules/playground/e2e_test/model_driven_forms/model_driven_forms_spec.ts b/modules/playground/e2e_test/model_driven_forms/model_driven_forms_spec.ts index 74fd2d80ebbf83..bb6280fc830d70 100644 --- a/modules/playground/e2e_test/model_driven_forms/model_driven_forms_spec.ts +++ b/modules/playground/e2e_test/model_driven_forms/model_driven_forms_spec.ts @@ -6,10 +6,9 @@ * found in the LICENSE file at https://angular.io/license */ +import {verifyNoBrowserErrors} from '@angular/dev-infra-private/bazel/benchmark/driver-utilities'; import {browser, by, element} from 'protractor'; -import {verifyNoBrowserErrors} from '../../../../dev-infra/benchmark/driver-utilities'; - describe('Model-Driven Forms', function() { afterEach(verifyNoBrowserErrors); diff --git a/modules/playground/e2e_test/order_management/order_management_spec.ts b/modules/playground/e2e_test/order_management/order_management_spec.ts index 77bfa8ecbbc51b..d035e2aa9536e2 100644 --- a/modules/playground/e2e_test/order_management/order_management_spec.ts +++ b/modules/playground/e2e_test/order_management/order_management_spec.ts @@ -6,10 +6,9 @@ * found in the LICENSE file at https://angular.io/license */ +import {verifyNoBrowserErrors} from '@angular/dev-infra-private/bazel/benchmark/driver-utilities'; import {browser} from 'protractor'; -import {verifyNoBrowserErrors} from '../../../../dev-infra/benchmark/driver-utilities'; - describe('Order Management CRUD', function() { const URL = '/'; diff --git a/modules/playground/e2e_test/person_management/person_management_spec.ts b/modules/playground/e2e_test/person_management/person_management_spec.ts index ee46b4f465ab40..1434938c15c5f9 100644 --- a/modules/playground/e2e_test/person_management/person_management_spec.ts +++ b/modules/playground/e2e_test/person_management/person_management_spec.ts @@ -6,10 +6,9 @@ * found in the LICENSE file at https://angular.io/license */ +import {verifyNoBrowserErrors} from '@angular/dev-infra-private/bazel/benchmark/driver-utilities'; import {browser} from 'protractor'; -import {verifyNoBrowserErrors} from '../../../../dev-infra/benchmark/driver-utilities'; - describe('Person Management CRUD', function() { const URL = '/'; diff --git a/modules/playground/e2e_test/relative_assets/assets_spec.ts b/modules/playground/e2e_test/relative_assets/assets_spec.ts index ec102dd20747e4..98e231bce3a57e 100644 --- a/modules/playground/e2e_test/relative_assets/assets_spec.ts +++ b/modules/playground/e2e_test/relative_assets/assets_spec.ts @@ -6,10 +6,9 @@ * found in the LICENSE file at https://angular.io/license */ +import {verifyNoBrowserErrors} from '@angular/dev-infra-private/bazel/benchmark/driver-utilities'; import {$, browser, by, element, ExpectedConditions} from 'protractor'; -import {verifyNoBrowserErrors} from '../../../../dev-infra/benchmark/driver-utilities'; - function waitForElement(selector: string) { // Waits for the element with id 'abc' to be present on the dom. browser.wait(ExpectedConditions.presenceOf($(selector)), 20000); diff --git a/modules/playground/e2e_test/routing/routing_spec.ts b/modules/playground/e2e_test/routing/routing_spec.ts index 61911f21626ef7..d80033c30c8327 100644 --- a/modules/playground/e2e_test/routing/routing_spec.ts +++ b/modules/playground/e2e_test/routing/routing_spec.ts @@ -6,10 +6,9 @@ * found in the LICENSE file at https://angular.io/license */ +import {verifyNoBrowserErrors} from '@angular/dev-infra-private/bazel/benchmark/driver-utilities'; import {$, browser, by, element, ExpectedConditions} from 'protractor'; -import {verifyNoBrowserErrors} from '../../../../dev-infra/benchmark/driver-utilities'; - function waitForElement(selector: string) { // Waits for the element with id 'abc' to be present on the dom. browser.wait(ExpectedConditions.presenceOf($(selector)), 20000); diff --git a/modules/playground/e2e_test/svg/svg_spec.ts b/modules/playground/e2e_test/svg/svg_spec.ts index 93c306ed6a659d..15fbd7e2fbe04d 100644 --- a/modules/playground/e2e_test/svg/svg_spec.ts +++ b/modules/playground/e2e_test/svg/svg_spec.ts @@ -6,10 +6,9 @@ * found in the LICENSE file at https://angular.io/license */ +import {verifyNoBrowserErrors} from '@angular/dev-infra-private/bazel/benchmark/driver-utilities'; import {browser, by, element} from 'protractor'; -import {verifyNoBrowserErrors} from '../../../../dev-infra/benchmark/driver-utilities'; - describe('SVG', function() { const URL = '/'; diff --git a/modules/playground/e2e_test/template_driven_forms/template_driven_forms_spec.ts b/modules/playground/e2e_test/template_driven_forms/template_driven_forms_spec.ts index 5b984a526d4f4e..d50d9abb1d8505 100644 --- a/modules/playground/e2e_test/template_driven_forms/template_driven_forms_spec.ts +++ b/modules/playground/e2e_test/template_driven_forms/template_driven_forms_spec.ts @@ -6,10 +6,9 @@ * found in the LICENSE file at https://angular.io/license */ +import {verifyNoBrowserErrors} from '@angular/dev-infra-private/bazel/benchmark/driver-utilities'; import {browser, by, element} from 'protractor'; -import {verifyNoBrowserErrors} from '../../../../dev-infra/benchmark/driver-utilities'; - describe('Template-Driven Forms', function() { afterEach(verifyNoBrowserErrors); diff --git a/modules/playground/e2e_test/upgrade/upgrade_spec.ts b/modules/playground/e2e_test/upgrade/upgrade_spec.ts index 427ddc684c188e..12646bf42268a4 100644 --- a/modules/playground/e2e_test/upgrade/upgrade_spec.ts +++ b/modules/playground/e2e_test/upgrade/upgrade_spec.ts @@ -6,10 +6,9 @@ * found in the LICENSE file at https://angular.io/license */ +import {verifyNoBrowserErrors} from '@angular/dev-infra-private/bazel/benchmark/driver-utilities'; import {browser, by, element} from 'protractor'; -import {verifyNoBrowserErrors} from '../../../../dev-infra/benchmark/driver-utilities'; - describe('ngUpgrade', function() { const URL = '/'; diff --git a/modules/playground/e2e_test/zippy_component/zippy_spec.ts b/modules/playground/e2e_test/zippy_component/zippy_spec.ts index 3fa2eed5cc794e..c0ddfffe7b6e24 100644 --- a/modules/playground/e2e_test/zippy_component/zippy_spec.ts +++ b/modules/playground/e2e_test/zippy_component/zippy_spec.ts @@ -6,10 +6,9 @@ * found in the LICENSE file at https://angular.io/license */ +import {verifyNoBrowserErrors} from '@angular/dev-infra-private/bazel/benchmark/driver-utilities'; import {browser, by, element} from 'protractor'; -import {verifyNoBrowserErrors} from '../../../../dev-infra/benchmark/driver-utilities'; - describe('Zippy Component', function() { afterEach(verifyNoBrowserErrors); diff --git a/package.json b/package.json index 3cd30ec5f006c2..41aee5ad4581c1 100644 --- a/package.json +++ b/package.json @@ -35,9 +35,7 @@ "symbol-extractor:check": "node tools/symbol-extractor/run_all_symbols_extractor_tests.js test", "symbol-extractor:update": "node tools/symbol-extractor/run_all_symbols_extractor_tests.js accept", "ts-circular-deps:check": "yarn -s ng-dev ts-circular-deps check --config ./packages/circular-deps-test.conf.js", - "ts-circular-deps:approve": "yarn -s ng-dev ts-circular-deps approve --config ./packages/circular-deps-test.conf.js", - "ng-dev": "node dev-infra/ng-dev", - "ng-dev:dev": "ts-node --transpile-only -- dev-infra/cli.ts" + "ts-circular-deps:approve": "yarn -s ng-dev ts-circular-deps approve --config ./packages/circular-deps-test.conf.js" }, "// 1": "dependencies are used locally and by bazel", "dependencies": { @@ -163,6 +161,7 @@ }, "// 2": "devDependencies are not used under Bazel. Many can be removed after test.sh is deleted.", "devDependencies": { + "@angular/dev-infra-private": "https://github.com/angular/dev-infra-private-builds.git#a3bc7727bbceb6418e7b220900ea8b396cb82580", "@bazel/bazelisk": "^1.7.5", "@bazel/buildifier": "^4.0.1", "@bazel/ibazel": "^0.15.8", diff --git a/packages/language-service/bundles/BUILD.bazel b/packages/language-service/bundles/BUILD.bazel index edb3298c17fbda..1c4b11d0a17404 100644 --- a/packages/language-service/bundles/BUILD.bazel +++ b/packages/language-service/bundles/BUILD.bazel @@ -1,4 +1,4 @@ -load("//dev-infra/benchmark/ng_rollup_bundle:ng_rollup_bundle.bzl", "ng_rollup_bundle") +load("@npm//@angular/dev-infra-private/bazel/benchmark/ng_rollup_bundle:ng_rollup_bundle.bzl", "ng_rollup_bundle") ng_rollup_bundle( name = "language-service", diff --git a/packages/zone.js/test/browser/browser.spec.ts b/packages/zone.js/test/browser/browser.spec.ts index b73ddbf351b7ed..7c88afcb0e14b0 100644 --- a/packages/zone.js/test/browser/browser.spec.ts +++ b/packages/zone.js/test/browser/browser.spec.ts @@ -959,7 +959,7 @@ describe('Zone', function() { button.dispatchEvent(clickEvent); expect(logs.length).toBe(2); - expect(logs).toEqual(['click', 'once click']); + expect(logs).toEqual(['once click', 'click']); logs = []; button.dispatchEvent(clickEvent); diff --git a/packages/zone.js/test/karma_test.bzl b/packages/zone.js/test/karma_test.bzl index c47a7ea79c6264..a7073bfa7b1f44 100644 --- a/packages/zone.js/test/karma_test.bzl +++ b/packages/zone.js/test/karma_test.bzl @@ -70,7 +70,7 @@ def karma_test(name, env_srcs, env_deps, env_entry_point, test_srcs, test_deps, ":" + name + "_env_rollup.umd", ] + bootstrap + _karma_test_required_dist_files, - browsers = ["//dev-infra/bazel/browsers/chromium:chromium"], + browsers = ["@npm//@angular/dev-infra-private/bazel/browsers/chromium:chromium"], static_files = [ ":assets/sample.json", ":assets/worker.js", @@ -94,7 +94,7 @@ def karma_test(name, env_srcs, env_deps, env_entry_point, test_srcs, test_deps, ":" + name + "_env_rollup.umd", "//packages/zone.js/bundles:zone-testing-bundle.umd.min.js", ] + _karma_test_required_dist_files, - browsers = ["//dev-infra/bazel/browsers/chromium:chromium"], + browsers = ["@npm//@angular/dev-infra-private/bazel/browsers/chromium:chromium"], config_file = "//:karma-js.conf.js", configuration_env_vars = ["KARMA_WEB_TEST_MODE"], data = [ diff --git a/tools/defaults.bzl b/tools/defaults.bzl index 96a34a5b27af98..b0549c382a5999 100644 --- a/tools/defaults.bzl +++ b/tools/defaults.bzl @@ -10,10 +10,10 @@ load("@npm//@bazel/typescript:index.bzl", _ts_config = "ts_config", _ts_library load("@npm//@bazel/protractor:index.bzl", _protractor_web_test_suite = "protractor_web_test_suite") load("@npm//typescript:index.bzl", "tsc") load("//packages/bazel:index.bzl", _ng_module = "ng_module", _ng_package = "ng_package") -load("//dev-infra/benchmark/ng_rollup_bundle:ng_rollup_bundle.bzl", _ng_rollup_bundle = "ng_rollup_bundle") +load("@npm//@angular/dev-infra-private/bazel/benchmark/ng_rollup_bundle:ng_rollup_bundle.bzl", _ng_rollup_bundle = "ng_rollup_bundle") load("//tools:ng_benchmark.bzl", _ng_benchmark = "ng_benchmark") -load("//dev-infra/bazel/api-golden:index.bzl", _api_golden_test = "api_golden_test", _api_golden_test_npm_package = "api_golden_test_npm_package") -load("//dev-infra/bazel:extract_js_module_output.bzl", "extract_js_module_output") +load("@npm//@angular/dev-infra-private/bazel/api-golden:index.bzl", _api_golden_test = "api_golden_test", _api_golden_test_npm_package = "api_golden_test_npm_package") +load("@npm//@angular/dev-infra-private/bazel:extract_js_module_output.bzl", "extract_js_module_output") _DEFAULT_TSCONFIG_TEST = "//packages:tsconfig-test" _INTERNAL_NG_MODULE_API_EXTRACTOR = "//packages/bazel/src/api-extractor:api_extractor" @@ -335,8 +335,8 @@ def karma_web_test_suite(name, **kwargs): bootstrap = bootstrap, deps = deps, browsers = [ - "//dev-infra/bazel/browsers/chromium:chromium", - "//dev-infra/bazel/browsers/firefox:firefox", + "@npm//@angular/dev-infra-private/bazel/browsers/chromium:chromium", + "@npm//@angular/dev-infra-private/bazel/browsers/firefox:firefox", ], data = data, tags = tags, @@ -374,7 +374,7 @@ def protractor_web_test_suite(**kwargs): """Default values for protractor_web_test_suite""" _protractor_web_test_suite( - browsers = ["//dev-infra/bazel/browsers/chromium:chromium"], + browsers = ["@npm//@angular/dev-infra-private/bazel/browsers/chromium:chromium"], **kwargs ) diff --git a/tools/postinstall-patches.js b/tools/postinstall-patches.js index 45e9bdd03e1279..1ed30ae8c2063b 100644 --- a/tools/postinstall-patches.js +++ b/tools/postinstall-patches.js @@ -61,6 +61,20 @@ ls('node_modules/@types').filter(f => f.startsWith('babel__')).forEach(pkg => { } }); +log('\n# patch: use local version of @angular/* and zone.js in component_benchmark from @angular/dev-infra.-private'); +[['@npm//@angular/platform-browser', '@angular//packages/platform-browser'], + ['@npm//@angular/core', '@angular//packages/core'], + [ + 'load\\("@npm//@angular/bazel:index.bzl", "ng_module"\\)', + 'load\("@angular//tools:defaults.bzl", "ng_module"\)' + ], + ['@npm//zone.js', '//packages/zone.js/bundles:zone.umd.js'], + +].forEach(([matcher, replacement]) => { + sed('-i', matcher, replacement, + 'node_modules/@angular/dev-infra-private/bazel/benchmark/component_benchmark/component_benchmark.bzl'); +}); + log('\n# patch: delete d.ts files refering to rxjs-compat'); // more info in https://github.com/angular/angular/pull/33786 rm('-rf', [ diff --git a/tsconfig-tslint.json b/tsconfig-tslint.json index e5a235c7c904f4..7c9ad1ac928df8 100644 --- a/tsconfig-tslint.json +++ b/tsconfig-tslint.json @@ -3,7 +3,6 @@ "allowJs": true }, "include": [ - "dev-infra/**/*", "packages/**/*", "modules/**/*", "tools/**/*", diff --git a/tslint.json b/tslint.json index 2f812a798aaad3..b89819d1c24047 100644 --- a/tslint.json +++ b/tslint.json @@ -1,7 +1,7 @@ { "rulesDirectory": [ "tools/tslint", - "dev-infra/tslint-rules", + "node_modules/@angular/dev-infra-private/tslint-rules", "node_modules/vrsource-tslint-rules/rules", "node_modules/tslint-eslint-rules/dist/rules", "node_modules/tslint-no-toplevel-property-access/rules" @@ -120,9 +120,7 @@ "./modules/benchmarks_external/**/*", // Ignore zone.js directory // TODO(JiaLiPassion): add zone.js back later - "./packages/zone.js/**/*", - "./dev-infra/ng-dev.js", - "./dev-infra/build-worker.js" + "./packages/zone.js/**/*" ] } } diff --git a/yarn.lock b/yarn.lock index f43df02732690d..080d3d0d559e72 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,6 +2,28 @@ # yarn lockfile v1 +"@actions/core@^1.4.0": + version "1.4.0" + resolved "https://registry.yarnpkg.com/@actions/core/-/core-1.4.0.tgz#cf2e6ee317e314b03886adfeb20e448d50d6e524" + integrity sha512-CGx2ilGq5i7zSLgiiGUtBCxhRRxibJYU6Fim0Q1Wg2aQL2LTnF27zbqZOrxfvFQ55eSBW0L8uVStgtKMpa0Qlg== + +"@actions/github@^5.0.0": + version "5.0.0" + resolved "https://registry.yarnpkg.com/@actions/github/-/github-5.0.0.tgz#1754127976c50bd88b2e905f10d204d76d1472f8" + integrity sha512-QvE9eAAfEsS+yOOk0cylLBIO/d6WyWIOvsxxzdrPFaud39G6BOkUwScXZn1iBzQzHyu9SBkkLSWlohDWdsasAQ== + dependencies: + "@actions/http-client" "^1.0.11" + "@octokit/core" "^3.4.0" + "@octokit/plugin-paginate-rest" "^2.13.3" + "@octokit/plugin-rest-endpoint-methods" "^5.1.1" + +"@actions/http-client@^1.0.11": + version "1.0.11" + resolved "https://registry.yarnpkg.com/@actions/http-client/-/http-client-1.0.11.tgz#c58b12e9aa8b159ee39e7dd6cbd0e91d905633c0" + integrity sha512-VRYHGQV1rqnROJqdMvGUbY/Kn8vriQe/F9HR2AlYHzmKuM/p3kjNuXhmdBfcVgsvRWTz5C5XW5xvndZrVBuAYg== + dependencies: + tunnel "0.0.6" + "@angular-devkit/architect@0.1200.4": version "0.1200.4" resolved "https://registry.yarnpkg.com/@angular-devkit/architect/-/architect-0.1200.4.tgz#03b41bf94e39928de0eb35654aa4d2e59ac34418" @@ -102,6 +124,15 @@ typescript "3.2.4" webpack-sources "1.3.0" +"@angular-devkit/build-optimizer@^0.1201.3": + version "0.1201.4" + resolved "https://registry.yarnpkg.com/@angular-devkit/build-optimizer/-/build-optimizer-0.1201.4.tgz#79bf972b5905d7d193c838c496febfff6923ec2c" + integrity sha512-Hq+mDUe4xIyq4939JZaUkptsM89WnZOk8Qel6mS0T/bxMX/qs+nuGD5o+xDKkuayogbiTrLmyZBib0/90eSXEA== + dependencies: + source-map "0.7.3" + tslib "2.3.0" + typescript "4.3.4" + "@angular-devkit/build-webpack@0.1200.4": version "0.1200.4" resolved "https://registry.yarnpkg.com/@angular-devkit/build-webpack/-/build-webpack-0.1200.4.tgz#a957d706c7f1c449682a16c16998ccf06e406526" @@ -131,6 +162,14 @@ ora "5.4.0" rxjs "6.6.7" +"@angular/benchpress@0.2.1": + version "0.2.1" + resolved "https://registry.yarnpkg.com/@angular/benchpress/-/benchpress-0.2.1.tgz#f8b58d9acfda0d29959b87dcb8082b1c33735db5" + integrity sha512-ojHCP96ZunHBZpt08USSEdLJsuXnEEdJtfzl+9oTdMXbooKkzSVO7N6bVdjefbGRNAleAuSAo3gVrdPqumLznA== + dependencies: + "@angular/core" "^10.0.0-0 || ^11.0.0" + reflect-metadata "^0.1.13" + "@angular/cli@12.0.4": version "12.0.4" resolved "https://registry.yarnpkg.com/@angular/cli/-/cli-12.0.4.tgz#766d471e7486a9e51297278b3a8afdbc96265ab4" @@ -157,6 +196,86 @@ symbol-observable "4.0.0" uuid "8.3.2" +"@angular/core@^10.0.0-0 || ^11.0.0": + version "11.2.14" + resolved "https://registry.yarnpkg.com/@angular/core/-/core-11.2.14.tgz#3ebe298c79d5413dc670d56b7f503bd4d788d4a8" + integrity sha512-vpR4XqBGitk1Faph37CSpemwIYTmJ3pdIVNoHKP6jLonpWu+0azkchf0f7oD8/2ivj2F81opcIw0tcsy/D/5Vg== + dependencies: + tslib "^2.0.0" + +"@angular/dev-infra-private@https://github.com/angular/dev-infra-private-builds.git#a3bc7727bbceb6418e7b220900ea8b396cb82580": + version "0.0.0" + resolved "https://github.com/angular/dev-infra-private-builds.git#a3bc7727bbceb6418e7b220900ea8b396cb82580" + dependencies: + "@actions/core" "^1.4.0" + "@actions/github" "^5.0.0" + "@angular-devkit/build-optimizer" "^0.1201.3" + "@angular/benchpress" "0.2.1" + "@bazel/bazelisk" "^1.10.1" + "@bazel/buildifier" "^4.0.1" + "@bazel/esbuild" "4.0.0-beta.0" + "@bazel/jasmine" "4.0.0-beta.0" + "@bazel/protractor" "4.0.0-beta.0" + "@bazel/runfiles" "4.0.0-beta.0" + "@bazel/typescript" "4.0.0-beta.0" + "@microsoft/api-extractor" "7.18.4" + "@octokit/core" "^3.5.1" + "@octokit/graphql" "^4.6.1" + "@octokit/plugin-paginate-rest" "^2.13.5" + "@octokit/plugin-rest-endpoint-methods" "^5.3.3" + "@octokit/rest" "^18.7.0" + "@octokit/types" "^6.16.6" + "@types/chalk" "^2.2.0" + "@types/cli-progress" "^3.9.1" + "@types/conventional-commits-parser" "^3.0.1" + "@types/ejs" "^3.0.6" + "@types/events" "^3.0.0" + "@types/git-raw-commits" "^2.0.0" + "@types/glob" "^7.1.3" + "@types/inquirer" "7.3.1" + "@types/jasmine" "^3.7.0" + "@types/minimist" "^1.2.0" + "@types/node" "^14.17.0" + "@types/node-fetch" "^2.5.10" + "@types/semver" "^7.3.6" + "@types/shelljs" "^0.8.8" + "@types/yaml" "^1.9.7" + "@types/yargs" "^17.0.0" + brotli "^1.3.2" + chalk "^4.1.0" + clang-format "^1.4.0" + cli-progress "^3.7.0" + conventional-commits-parser "^3.2.1" + ejs "^3.1.6" + git-raw-commits "^2.0.10" + github-app-installation-token "^1.1.0" + glob "7.1.7" + husky "^7.0.1" + inquirer "^8.0.0" + jasmine "^3.7.0" + minimatch "^3.0.4" + minimist "^1.2.5" + multimatch "^5.0.0" + nock "^13.0.3" + node-fetch "^2.6.1" + node-uuid "1.4.8" + ora "^5.0.0" + prettier "^2.3.2" + protractor "^7.0.0" + rollup "^2.53.3" + rollup-plugin-commonjs "^10.1.0" + rollup-plugin-node-resolve "^5.2.0" + rollup-plugin-sourcemaps "^0.6.3" + selenium-webdriver "3.5.0" + semver "^7.3.5" + ts-node "^10.0.0" + tslib "^2.3.0" + tslint "^6.1.3" + typed-graphqlify "^3.1.1" + typescript "~4.3.5" + yaml "^1.10.0" + yargs "^17.0.0" + "@apidevtools/json-schema-ref-parser@^9.0.3": version "9.0.9" resolved "https://registry.yarnpkg.com/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-9.0.9.tgz#d720f9256e3609621280584f2b47ae165359268b" @@ -998,7 +1117,7 @@ lodash "^4.17.13" to-fast-properties "^2.0.0" -"@bazel/bazelisk@^1.7.5": +"@bazel/bazelisk@^1.10.1", "@bazel/bazelisk@^1.7.5": version "1.10.1" resolved "https://registry.yarnpkg.com/@bazel/bazelisk/-/bazelisk-1.10.1.tgz#46236a43ad58e310c55247f866da0dc6083c3d8b" integrity sha512-IHszNzBO2UrUy6YtsSAsZtnU6I6qpzXGkWdEvGoMxLgJnDsEnsIYniDCUjvjU1KAP+A03eepmCHlyFcRHMSxRA== @@ -1017,11 +1136,24 @@ source-map-support "0.5.9" tsutils "2.27.2" +"@bazel/esbuild@4.0.0-beta.0": + version "4.0.0-beta.0" + resolved "https://registry.yarnpkg.com/@bazel/esbuild/-/esbuild-4.0.0-beta.0.tgz#ff75447b18bc0d56b376d7e44095d9ca9b904583" + integrity sha512-4AxL8IhyeyeTH0fr1XFfdd1ls/AnsiEu1oBXxoplb0ar88pRrdl0UjCUgLylWj75uIcQsqu/l3Xv7qOfDSXWsQ== + "@bazel/ibazel@^0.15.8": version "0.15.10" resolved "https://registry.yarnpkg.com/@bazel/ibazel/-/ibazel-0.15.10.tgz#cf0cff1aec6d8e7bb23e1fc618d09fbd39b7a13f" integrity sha512-0v+OwCQ6fsGFa50r6MXWbUkSGuWOoZ22K4pMSdtWiL5LKFIE4kfmMmtQS+M7/ICNwk2EIYob+NRreyi/DGUz5A== +"@bazel/jasmine@4.0.0-beta.0": + version "4.0.0-beta.0" + resolved "https://registry.yarnpkg.com/@bazel/jasmine/-/jasmine-4.0.0-beta.0.tgz#575e9cd88932b19c54447fba43cbd62e14ff3022" + integrity sha512-VYEAaHCi8ot0aSMKbIio6usJZqvINd7LzrrGoChGAGwMgGEpTfMGdprwdlrQPKD6/GbLkVtkCcsViG92ynXhdQ== + dependencies: + c8 "~7.5.0" + jasmine-reporters "~2.4.0" + "@bazel/jasmine@4.0.0-beta.1": version "4.0.0-beta.1" resolved "https://registry.yarnpkg.com/@bazel/jasmine/-/jasmine-4.0.0-beta.1.tgz#a4a7eea7f83c83cbdbc6ecd1d0878d02e0f1fede" @@ -1030,6 +1162,11 @@ c8 "~7.5.0" jasmine-reporters "~2.4.0" +"@bazel/protractor@4.0.0-beta.0": + version "4.0.0-beta.0" + resolved "https://registry.yarnpkg.com/@bazel/protractor/-/protractor-4.0.0-beta.0.tgz#fcabfd5c32005fcb93b80f83bcf99058bcf07d4f" + integrity sha512-cIlqzPEXu3zFhFR+5Vqo5D/qLkOEY/gZ1xc74/V/CVAlbkCZsWJ18gDE1bhca9t1Mj41igDqwlvXUndxdQjNtw== + "@bazel/protractor@4.0.0-beta.1": version "4.0.0-beta.1" resolved "https://registry.yarnpkg.com/@bazel/protractor/-/protractor-4.0.0-beta.1.tgz#684bd9ee26265aabf6417b2c6301d12bb837742b" @@ -1040,6 +1177,11 @@ resolved "https://registry.yarnpkg.com/@bazel/rollup/-/rollup-4.0.0-beta.1.tgz#df33f7ddb643f27e8ef6ac04a6c93f0d872884ce" integrity sha512-a9+kVGCbFqLlxVoECFdlmnR82iY9mZ/4Jnjw3rX3h95uwsNn7apPbq/GrWxptMmvTrMr49O+rmn6qUQQEHUhKQ== +"@bazel/runfiles@4.0.0-beta.0": + version "4.0.0-beta.0" + resolved "https://registry.yarnpkg.com/@bazel/runfiles/-/runfiles-4.0.0-beta.0.tgz#e62679d80cf9fcd84996e5f3ae4bedc33ed1a993" + integrity sha512-pFdanyvI0wf2WtdQXUmcTZw7OJ83uj2bxF3rOskx45wewBRAlQZkm2q2A6WEffSfdf2WaBlk5u/x2kqK2nyG7w== + "@bazel/runfiles@4.0.0-beta.1": version "4.0.0-beta.1" resolved "https://registry.yarnpkg.com/@bazel/runfiles/-/runfiles-4.0.0-beta.1.tgz#4893c58166e5bb6ebd7cb80dfb99e99923337903" @@ -1050,6 +1192,16 @@ resolved "https://registry.yarnpkg.com/@bazel/terser/-/terser-4.0.0-beta.1.tgz#3b9d5dc69b87f781b94bf76dfa59b61a8c5a41e4" integrity sha512-Kgcxqt+oxuNli0IvGF9/edkOTiM0hY+h6Z07UoYmnPsbrGcdVaUXjO8hplmT2G/acMZIeH6P1SJg7eusVnKcUg== +"@bazel/typescript@4.0.0-beta.0": + version "4.0.0-beta.0" + resolved "https://registry.yarnpkg.com/@bazel/typescript/-/typescript-4.0.0-beta.0.tgz#daf2fb0d0bdcd3df331d4320463b1a4beca6031c" + integrity sha512-dfI3QLQ5bLyK3BIrrwxuH9cLbMHuku0UZH7nwjHjXwQX70O0MSGN6yXx2Vrqatj2iw/UfHX/akPuhGlNqrDFRw== + dependencies: + protobufjs "6.8.8" + semver "5.6.0" + source-map-support "0.5.9" + tsutils "2.27.2" + "@bazel/typescript@4.0.0-beta.1": version "4.0.0-beta.1" resolved "https://registry.yarnpkg.com/@bazel/typescript/-/typescript-4.0.0-beta.1.tgz#2524d3d0e4cba56ccc5b8a6ab0a1ee8af5a86b70" @@ -1172,6 +1324,15 @@ resolved "https://registry.yarnpkg.com/@jsdevtools/ono/-/ono-7.1.3.tgz#9df03bbd7c696a5c58885c34aa06da41c8543796" integrity sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg== +"@microsoft/api-extractor-model@7.13.4": + version "7.13.4" + resolved "https://registry.yarnpkg.com/@microsoft/api-extractor-model/-/api-extractor-model-7.13.4.tgz#bff4a52a35da5d9896650041d4f7a769c970da60" + integrity sha512-NYaR3hJinh089/Gkee8fvmEFf9zKkoUvNxgkqUlKBCDXH2+Ou4tNDuL8G6zjhKBPicHkp2VcL8l7q9H6txUkjQ== + dependencies: + "@microsoft/tsdoc" "0.13.2" + "@microsoft/tsdoc-config" "~0.15.2" + "@rushstack/node-core-library" "3.39.1" + "@microsoft/api-extractor-model@7.13.5": version "7.13.5" resolved "https://registry.yarnpkg.com/@microsoft/api-extractor-model/-/api-extractor-model-7.13.5.tgz#7836a81ba47b9a654062ed0361e4eee69afae51e" @@ -1181,6 +1342,24 @@ "@microsoft/tsdoc-config" "~0.15.2" "@rushstack/node-core-library" "3.40.0" +"@microsoft/api-extractor@7.18.4": + version "7.18.4" + resolved "https://registry.yarnpkg.com/@microsoft/api-extractor/-/api-extractor-7.18.4.tgz#2d7641b36d323b4ac710d838a972be7e4f14d32b" + integrity sha512-Wx45VuIAu09Pk9Qwzt0I57OX31BaWO2r6+mfSXqYFsJjYTqwUkdFh92G1GKYgvuR9oF/ai7w10wrFpx5WZYbGg== + dependencies: + "@microsoft/api-extractor-model" "7.13.4" + "@microsoft/tsdoc" "0.13.2" + "@microsoft/tsdoc-config" "~0.15.2" + "@rushstack/node-core-library" "3.39.1" + "@rushstack/rig-package" "0.2.13" + "@rushstack/ts-command-line" "4.8.1" + colors "~1.2.1" + lodash "~4.17.15" + resolve "~1.17.0" + semver "~7.3.0" + source-map "~0.6.1" + typescript "~4.3.5" + "@microsoft/api-extractor@7.18.5": version "7.18.5" resolved "https://registry.yarnpkg.com/@microsoft/api-extractor/-/api-extractor-7.18.5.tgz#cc2804d7c8b9d0f1e63fd85d0448569b767db102" @@ -1312,6 +1491,19 @@ node-gyp "^7.1.0" read-package-json-fast "^2.0.1" +"@octokit/auth-app@2.8.0": + version "2.8.0" + resolved "https://registry.yarnpkg.com/@octokit/auth-app/-/auth-app-2.8.0.tgz#51b74043e1a5e0c4e5924d9bf97fcf26883f6d5a" + integrity sha512-FPTygbzdS7HkxgAhpbwTYqzimb3QpCMP4IuykkatkAet77jDQhAHn7DXFDowdZMilHuEBWQrVerFAH8yb0rHtg== + dependencies: + "@octokit/request" "^5.3.0" + "@octokit/request-error" "^2.0.0" + "@octokit/types" "^5.0.0" + "@types/lru-cache" "^5.1.0" + lru-cache "^6.0.0" + universal-github-app-jwt "^1.0.1" + universal-user-agent "^6.0.0" + "@octokit/auth-token@^2.4.4": version "2.4.5" resolved "https://registry.yarnpkg.com/@octokit/auth-token/-/auth-token-2.4.5.tgz#568ccfb8cb46f36441fac094ce34f7a875b197f3" @@ -1319,7 +1511,7 @@ dependencies: "@octokit/types" "^6.0.3" -"@octokit/core@^3.5.0", "@octokit/core@^3.5.1": +"@octokit/core@^3.2.3", "@octokit/core@^3.4.0", "@octokit/core@^3.5.0", "@octokit/core@^3.5.1": version "3.5.1" resolved "https://registry.yarnpkg.com/@octokit/core/-/core-3.5.1.tgz#8601ceeb1ec0e1b1b8217b960a413ed8e947809b" integrity sha512-omncwpLVxMP+GLpLPgeGJBF6IWJFjXDS5flY5VbppePYX9XehevbDykRH9PdCdvqt9TS5AOTiDide7h0qrkHjw== @@ -1355,6 +1547,18 @@ resolved "https://registry.yarnpkg.com/@octokit/openapi-types/-/openapi-types-9.2.0.tgz#46bbfe6a85bfd2987e69216955fcd04df7d025bb" integrity sha512-c4A1Xm0At+ypvBfEETREu519wLncJYQXvY+dBGg/V5YA51eg5EwdDsPPfcOMG0cuXscqRvsIgIySTmTJUdcTNA== +"@octokit/openapi-types@^9.5.0": + version "9.7.0" + resolved "https://registry.yarnpkg.com/@octokit/openapi-types/-/openapi-types-9.7.0.tgz#9897cdefd629cd88af67b8dbe2e5fb19c63426b2" + integrity sha512-TUJ16DJU8mekne6+KVcMV5g6g/rJlrnIKn7aALG9QrNpnEipFc1xjoarh0PKaAWf2Hf+HwthRKYt+9mCm5RsRg== + +"@octokit/plugin-paginate-rest@^2.13.3": + version "2.15.1" + resolved "https://registry.yarnpkg.com/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-2.15.1.tgz#264189dd3ce881c6c33758824aac05a4002e056a" + integrity sha512-47r52KkhQDkmvUKZqXzA1lKvcyJEfYh3TKAIe5+EzMeyDM3d+/s5v11i2gTk8/n6No6DPi3k5Ind6wtDbo/AEg== + dependencies: + "@octokit/types" "^6.24.0" + "@octokit/plugin-paginate-rest@^2.13.5", "@octokit/plugin-paginate-rest@^2.6.2": version "2.14.0" resolved "https://registry.yarnpkg.com/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-2.14.0.tgz#f469cb4a908792fb44679c5973d8bba820c88b0f" @@ -1367,6 +1571,14 @@ resolved "https://registry.yarnpkg.com/@octokit/plugin-request-log/-/plugin-request-log-1.0.4.tgz#5e50ed7083a613816b1e4a28aeec5fb7f1462e85" integrity sha512-mLUsMkgP7K/cnFEw07kWqXGF5LKrOkD+lhCrKvPHXWDywAwuDUeDwWBpc69XK3pNX0uKiVt8g5z96PJ6z9xCFA== +"@octokit/plugin-rest-endpoint-methods@4.4.1": + version "4.4.1" + resolved "https://registry.yarnpkg.com/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-4.4.1.tgz#105cf93255432155de078c9efc33bd4e14d1cd63" + integrity sha512-+v5PcvrUcDeFXf8hv1gnNvNLdm4C0+2EiuWt9EatjjUmfriM1pTMM+r4j1lLHxeBQ9bVDmbywb11e3KjuavieA== + dependencies: + "@octokit/types" "^6.1.0" + deprecation "^2.3.1" + "@octokit/plugin-rest-endpoint-methods@5.5.2", "@octokit/plugin-rest-endpoint-methods@^5.3.3": version "5.5.2" resolved "https://registry.yarnpkg.com/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-5.5.2.tgz#c8bdb3065a9725e30802295f10a31b3ff434830c" @@ -1375,7 +1587,23 @@ "@octokit/types" "^6.22.0" deprecation "^2.3.1" -"@octokit/request-error@^2.0.5", "@octokit/request-error@^2.1.0": +"@octokit/plugin-rest-endpoint-methods@5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-5.7.0.tgz#80b69452c17597738d4692c79829b72d9e72ccec" + integrity sha512-G7sgccWRYQMwcHJXkDY/sDxbXeKiZkFQqUtzBCwmrzCNj2GQf3VygQ4T/BFL2crLVpIbenkE/c0ErhYOte2MPw== + dependencies: + "@octokit/types" "^6.24.0" + deprecation "^2.3.1" + +"@octokit/plugin-rest-endpoint-methods@^5.1.1": + version "5.8.0" + resolved "https://registry.yarnpkg.com/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-5.8.0.tgz#33b342fe41f2603fdf8b958e6652103bb3ea3f3b" + integrity sha512-qeLZZLotNkoq+it6F+xahydkkbnvSK0iDjlXFo3jNTB+Ss0qIbYQb9V/soKLMkgGw8Q2sHjY5YEXiA47IVPp4A== + dependencies: + "@octokit/types" "^6.25.0" + deprecation "^2.3.1" + +"@octokit/request-error@^2.0.0", "@octokit/request-error@^2.0.5", "@octokit/request-error@^2.1.0": version "2.1.0" resolved "https://registry.yarnpkg.com/@octokit/request-error/-/request-error-2.1.0.tgz#9e150357831bfc788d13a4fd4b1913d60c74d677" integrity sha512-1VIvgXxs9WHSjicsRwq8PlR2LR2x6DwsJAaFgzdi0JfJoGSO8mYI/cHJQ+9FbN21aa+DrgNLnwObmyeSC8Rmpg== @@ -1384,6 +1612,18 @@ deprecation "^2.0.0" once "^1.4.0" +"@octokit/request@^5.3.0": + version "5.6.1" + resolved "https://registry.yarnpkg.com/@octokit/request/-/request-5.6.1.tgz#f97aff075c37ab1d427c49082fefeef0dba2d8ce" + integrity sha512-Ls2cfs1OfXaOKzkcxnqw5MR6drMA/zWX/LIS/p8Yjdz7QKTPQLMsB3R+OvoxE6XnXeXEE2X7xe4G4l4X0gRiKQ== + dependencies: + "@octokit/endpoint" "^6.0.1" + "@octokit/request-error" "^2.1.0" + "@octokit/types" "^6.16.1" + is-plain-object "^5.0.0" + node-fetch "^2.6.1" + universal-user-agent "^6.0.0" + "@octokit/request@^5.6.0": version "5.6.0" resolved "https://registry.yarnpkg.com/@octokit/request/-/request-5.6.0.tgz#6084861b6e4fa21dc40c8e2a739ec5eff597e672" @@ -1396,6 +1636,16 @@ node-fetch "^2.6.1" universal-user-agent "^6.0.0" +"@octokit/rest@18.0.12": + version "18.0.12" + resolved "https://registry.yarnpkg.com/@octokit/rest/-/rest-18.0.12.tgz#278bd41358c56d87c201e787e8adc0cac132503a" + integrity sha512-hNRCZfKPpeaIjOVuNJzkEL6zacfZlBPV8vw8ReNeyUkVvbuCvvrrx8K8Gw2eyHHsmd4dPlAxIXIZ9oHhJfkJpw== + dependencies: + "@octokit/core" "^3.2.3" + "@octokit/plugin-paginate-rest" "^2.6.2" + "@octokit/plugin-request-log" "^1.0.2" + "@octokit/plugin-rest-endpoint-methods" "4.4.1" + "@octokit/rest@^18.6.2": version "18.7.2" resolved "https://registry.yarnpkg.com/@octokit/rest/-/rest-18.7.2.tgz#8239b5acd40fccb3f5d074e7a4386980f3770821" @@ -1406,6 +1656,23 @@ "@octokit/plugin-request-log" "^1.0.2" "@octokit/plugin-rest-endpoint-methods" "5.5.2" +"@octokit/rest@^18.7.0": + version "18.9.0" + resolved "https://registry.yarnpkg.com/@octokit/rest/-/rest-18.9.0.tgz#e5cc23fa199a2bdeea9efbe6096f81d7d6156fe9" + integrity sha512-VrmrE8gjpuOoDAGjrQq2j9ZhOE6LxaqxaQg0yMrrEnnQZy2ZcAnr5qbVfKsMF0up/48PRV/VFS/2GSMhA7nTdA== + dependencies: + "@octokit/core" "^3.5.0" + "@octokit/plugin-paginate-rest" "^2.6.2" + "@octokit/plugin-request-log" "^1.0.2" + "@octokit/plugin-rest-endpoint-methods" "5.7.0" + +"@octokit/types@^5.0.0": + version "5.5.0" + resolved "https://registry.yarnpkg.com/@octokit/types/-/types-5.5.0.tgz#e5f06e8db21246ca102aa28444cdb13ae17a139b" + integrity sha512-UZ1pErDue6bZNjYOotCNveTXArOMZQFG6hKJfOnGnulVCMcVVi7YIIuuR4WfBhjo7zgpmzn/BkPDnUXtNx+PcQ== + dependencies: + "@types/node" ">= 8" + "@octokit/types@^6.0.3", "@octokit/types@^6.16.1", "@octokit/types@^6.16.6", "@octokit/types@^6.18.0", "@octokit/types@^6.22.0": version "6.22.0" resolved "https://registry.yarnpkg.com/@octokit/types/-/types-6.22.0.tgz#389bade20955c919241b6ffb9dd33f6e0cf1cc6c" @@ -1413,6 +1680,13 @@ dependencies: "@octokit/openapi-types" "^9.2.0" +"@octokit/types@^6.1.0", "@octokit/types@^6.24.0", "@octokit/types@^6.25.0": + version "6.25.0" + resolved "https://registry.yarnpkg.com/@octokit/types/-/types-6.25.0.tgz#c8e37e69dbe7ce55ed98ee63f75054e7e808bf1a" + integrity sha512-bNvyQKfngvAd/08COlYIN54nRgxskmejgywodizQNyiKoXmWRAjKup2/LYwm+T9V0gsKH6tuld1gM0PzmOiB4Q== + dependencies: + "@octokit/openapi-types" "^9.5.0" + "@opentelemetry/api@^1.0.0": version "1.0.2" resolved "https://registry.yarnpkg.com/@opentelemetry/api/-/api-1.0.2.tgz#921e1f2b2484b762d77225a8a25074482d93fccf" @@ -1485,6 +1759,21 @@ estree-walker "^1.0.1" picomatch "^2.2.2" +"@rushstack/node-core-library@3.39.1": + version "3.39.1" + resolved "https://registry.yarnpkg.com/@rushstack/node-core-library/-/node-core-library-3.39.1.tgz#dd1dc270e3035ac4de270f0ca80c25724ce19cc7" + integrity sha512-HHgMEHZTXQ3NjpQzWd5+fSt2Eod9yFwj6qBPbaeaNtDNkOL8wbLoxVimQNtcH0Qhn4wxF5u2NTDNFsxf2yd1jw== + dependencies: + "@types/node" "10.17.13" + colors "~1.2.1" + fs-extra "~7.0.1" + import-lazy "~4.0.0" + jju "~1.4.0" + resolve "~1.17.0" + semver "~7.3.0" + timsort "~0.3.0" + z-schema "~3.18.3" + "@rushstack/node-core-library@3.40.0": version "3.40.0" resolved "https://registry.yarnpkg.com/@rushstack/node-core-library/-/node-core-library-3.40.0.tgz#2551915ea34e34ec2abb7172b9d7f4546144d9d4" @@ -1651,12 +1940,19 @@ resolved "https://registry.yarnpkg.com/@types/bluebird/-/bluebird-3.5.36.tgz#00d9301d4dc35c2f6465a8aec634bb533674c652" integrity sha512-HBNx4lhkxN7bx6P0++W8E289foSu8kO8GCk2unhuVggO+cE7rh9DhZUyPhUxNRG9m+5B5BTKxZQ5ZP92x/mx9Q== +"@types/chalk@^2.2.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@types/chalk/-/chalk-2.2.0.tgz#b7f6e446f4511029ee8e3f43075fb5b73fbaa0ba" + integrity sha512-1zzPV9FDe1I/WHhRkf9SNgqtRJWZqrBWgu7JGveuHmmyR9CnAPCie2N/x+iHrgnpYBIcCJWHBoMRv2TRWktsvw== + dependencies: + chalk "*" + "@types/cldrjs@^0.4.22": version "0.4.22" resolved "https://registry.yarnpkg.com/@types/cldrjs/-/cldrjs-0.4.22.tgz#24e31cdf15a4ea806ca0a774b024150d1066fea1" integrity sha512-YyzxXZ5s9xwPWznXnI3++X14JGnomDdDAlin7kWZvxX/MzirC9BNFcDSQ0yR8YG2M/xNMn0nXsCGkgbFVyXjGw== -"@types/cli-progress@^3.4.2": +"@types/cli-progress@^3.4.2", "@types/cli-progress@^3.9.1": version "3.9.2" resolved "https://registry.yarnpkg.com/@types/cli-progress/-/cli-progress-3.9.2.tgz#6ca355f96268af39bee9f9307f0ac96145639c26" integrity sha512-VO5/X5Ij+oVgEVjg5u0IXVe3JQSKJX+Ev8C5x+0hPy0AuWyW+bF8tbajR7cPFnDGhs7pidztcac+ccrDtk5teA== @@ -1723,7 +2019,7 @@ resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.47.tgz#d7a51db20f0650efec24cd04994f523d93172ed4" integrity sha512-c5ciR06jK8u9BstrmJyO97m+klJrrhCf9u3rLu3DEAJBirxRqSCvDQoYKmxuYwQI5SZChAWu+tq9oVlGRuzPAg== -"@types/events@3.0.0": +"@types/events@3.0.0", "@types/events@^3.0.0": version "3.0.0" resolved "https://registry.yarnpkg.com/@types/events/-/events-3.0.0.tgz#2862f3f58a9a7f7c3e78d79f130dd4d71c25c2a7" integrity sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g== @@ -1735,7 +2031,7 @@ dependencies: "@types/node" "*" -"@types/glob@*", "@types/glob@^7.1.1": +"@types/glob@*", "@types/glob@^7.1.1", "@types/glob@^7.1.3": version "7.1.4" resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.1.4.tgz#ea59e21d2ee5c517914cb4bc8e4153b99e566672" integrity sha512-w+LsMxKyYQm347Otw+IfBXOv9UWVjpHpCDdbBMt8Kz/xbvCYNjP+0qPh91Km3iKfSRLBB0P7fAMf0KHrPu+MyA== @@ -1748,6 +2044,14 @@ resolved "https://registry.yarnpkg.com/@types/hammerjs/-/hammerjs-2.0.40.tgz#ded0240b6ea1ad7afc1e60374c49087aaea5dbd8" integrity sha512-VbjwR1fhsn2h2KXAY4oy1fm7dCxaKy0D+deTb8Ilc3Eo3rc5+5eA4rfYmZaHgNJKxVyI0f6WIXzO2zLkVmQPHA== +"@types/inquirer@7.3.1": + version "7.3.1" + resolved "https://registry.yarnpkg.com/@types/inquirer/-/inquirer-7.3.1.tgz#1f231224e7df11ccfaf4cf9acbcc3b935fea292d" + integrity sha512-osD38QVIfcdgsPCT0V3lD7eH0OFurX71Jft18bZrsVQWVRt6TuxRzlr0GJLrxoHZR2V5ph7/qP8se/dcnI7o0g== + dependencies: + "@types/through" "*" + rxjs "^6.4.0" + "@types/inquirer@^7.3.0": version "7.3.3" resolved "https://registry.yarnpkg.com/@types/inquirer/-/inquirer-7.3.3.tgz#92e6676efb67fa6925c69a2ee638f67a822952ac" @@ -1771,7 +2075,7 @@ resolved "https://registry.yarnpkg.com/@types/jasmine-ajax/-/jasmine-ajax-3.3.3.tgz#ec89515434d4aee9772e25a730bd626ecf44260d" integrity sha512-e2iJYrdwfxr6WmLHxNjISUgbrbGaRsFHsc9xnrq8SGFzbSYPDZ4lmAkIulAAjqrkZ1z21OJCZsZwWMnxoeX7+A== -"@types/jasmine@*", "@types/jasmine@3.8.2": +"@types/jasmine@*", "@types/jasmine@3.8.2", "@types/jasmine@^3.7.0": version "3.8.2" resolved "https://registry.yarnpkg.com/@types/jasmine/-/jasmine-3.8.2.tgz#27ab0aaac29581bcbde5774e1843f90df977078e" integrity sha512-u5h7dqzy2XpXTzhOzSNQUQpKGFvROF8ElNX9P/TJvsHnTg/JvsAseVsGWQAQQldqanYaM+5kwxW909BBFAUYsg== @@ -1788,11 +2092,23 @@ resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.8.tgz#edf1bf1dbf4e04413ca8e5b17b3b7d7d54b59818" integrity sha512-YSBPTLTVm2e2OoQIDYx8HaeWJ5tTToLH67kXR7zYNGupXMEHa2++G8k+DczX2cFVgalypqtyZIcU19AFcmOpmg== +"@types/jsonwebtoken@^8.3.3": + version "8.5.4" + resolved "https://registry.yarnpkg.com/@types/jsonwebtoken/-/jsonwebtoken-8.5.4.tgz#50ccaf0aa6f5d7b9956e70fe323b76e582991913" + integrity sha512-4L8msWK31oXwdtC81RmRBAULd0ShnAHjBuKT9MRQpjP0piNrZdXyTRcKY9/UIfhGeKIT4PvF5amOOUbbT/9Wpg== + dependencies: + "@types/node" "*" + "@types/long@^4.0.0", "@types/long@^4.0.1": version "4.0.1" resolved "https://registry.yarnpkg.com/@types/long/-/long-4.0.1.tgz#459c65fa1867dafe6a8f322c4c51695663cc55e9" integrity sha512-5tXH6Bx/kNGd3MgffdmP4dy2Z+G4eaXw0SE81Tq3BNadtnMR5/ySMzX4SLEzHJzSmPNn4HIdpQsBvXMUykr58w== +"@types/lru-cache@^5.1.0": + version "5.1.1" + resolved "https://registry.yarnpkg.com/@types/lru-cache/-/lru-cache-5.1.1.tgz#c48c2e27b65d2a153b19bfc1a317e30872e01eef" + integrity sha512-ssE3Vlrys7sdIzs5LOxCzTVMsU7i9oa/IaW92wF32JFb3CVczqOkru2xspuKczHEbG3nvmPY7IFqVmGGHdNbYw== + "@types/minimatch@*", "@types/minimatch@^3.0.3": version "3.0.5" resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.5.tgz#1001cc5e6a3704b83c236027e77f2f58ea010f40" @@ -1810,7 +2126,7 @@ dependencies: multimatch "*" -"@types/node-fetch@^2.5.7": +"@types/node-fetch@^2.5.10", "@types/node-fetch@^2.5.7": version "2.5.12" resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.5.12.tgz#8a6f779b1d4e60b7a57fb6fd48d84fb545b9cc66" integrity sha512-MKgC4dlq4kKNa/mYrwpKfzQMB5X3ee5U6fSprkKpToBqBmX4nFZL9cW5jl6sWn+xpRJ7ypWh2yyqqr8UUCstSw== @@ -1828,6 +2144,11 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-10.17.13.tgz#ccebcdb990bd6139cd16e84c39dc2fb1023ca90c" integrity sha512-pMCcqU2zT4TjqYFrWtYHKal7Sl30Ims6ulZ4UFXxI4xbtQqK/qqKwkDoBFCfooRqqmRu9vY3xaJRwxSh673aYg== +"@types/node@>= 8": + version "16.6.1" + resolved "https://registry.yarnpkg.com/@types/node/-/node-16.6.1.tgz#aee62c7b966f55fc66c7b6dfa1d58db2a616da61" + integrity sha512-Sr7BhXEAer9xyGuCN3Ek9eg9xPviCF2gfu9kTfuU2HkTVAMYSDeX40fvpmo72n5nansg3nsBjuQBrsS28r+NUw== + "@types/node@^10.1.0": version "10.17.60" resolved "https://registry.yarnpkg.com/@types/node/-/node-10.17.60.tgz#35f3d6213daed95da7f0f73e75bcc6980e90597b" @@ -1843,6 +2164,11 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-12.20.17.tgz#ffd44c2801fc527a6fe6e86bc9b900261df1c87e" integrity sha512-so8EHl4S6MmatPS0f9sE1ND94/ocbcEshW5OpyYthRqeRpiYyW2uXYTo/84kmfdfeNrDycARkvuiXl6nO40NGg== +"@types/node@^14.17.0": + version "14.17.9" + resolved "https://registry.yarnpkg.com/@types/node/-/node-14.17.9.tgz#b97c057e6138adb7b720df2bd0264b03c9f504fd" + integrity sha512-CMjgRNsks27IDwI785YMY0KLt3co/c0cQ5foxHYv/shC2w8oOnVwz5Ubq1QG5KzrcW+AXk6gzdnxIkDnTvzu3g== + "@types/normalize-package-data@^2.4.0": version "2.4.1" resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.1.tgz#d3357479a0fdfdd5907fe67e17e0a85c906e1301" @@ -1875,12 +2201,12 @@ resolved "https://registry.yarnpkg.com/@types/selenium-webdriver/-/selenium-webdriver-3.0.19.tgz#28ecede76f15b13553b4e86074d4cf9a0bbe49c4" integrity sha512-OFUilxQg+rWL2FMxtmIgCkUDlJB6pskkpvmew7yeXfzzsOBb5rc+y2+DjHm+r3r1ZPPcJefK3DveNSYWGiy68g== -"@types/semver@^7.3.4": +"@types/semver@^7.3.4", "@types/semver@^7.3.6": version "7.3.8" resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.3.8.tgz#508a27995498d7586dcecd77c25e289bfaf90c59" integrity sha512-D/2EJvAlCEtYFEYmmlGwbGXuK886HzyCc3nZX/tkFTQdEU8jZDAgiv08P162yB17y4ZXZoq7yFAnW4GDBb9Now== -"@types/shelljs@^0.8.6": +"@types/shelljs@^0.8.6", "@types/shelljs@^0.8.8": version "0.8.9" resolved "https://registry.yarnpkg.com/@types/shelljs/-/shelljs-0.8.9.tgz#45dd8501aa9882976ca3610517dac3831c2fbbf4" integrity sha512-flVe1dvlrCyQJN/SGrnBxqHG+RzXrVKsmjD8WS/qYHpq5UPjfq7UWFBENP0ZuOl0g6OpAlL6iBoLSvKYUUmyQw== @@ -1933,6 +2259,13 @@ dependencies: "@types/yargs-parser" "*" +"@types/yargs@^17.0.0": + version "17.0.2" + resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-17.0.2.tgz#8fb2e0f4cdc7ab2a1a570106e56533f31225b584" + integrity sha512-JhZ+pNdKMfB0rXauaDlrIvm+U7V4m03PPOSVoPS66z8gf+G4Z/UW8UlrVIj2MRQOBzuoEvYtjS0bqYwnpZaS9Q== + dependencies: + "@types/yargs-parser" "*" + "@types/yauzl@^2.9.1": version "2.9.2" resolved "https://registry.yarnpkg.com/@types/yauzl/-/yauzl-2.9.2.tgz#c48e5d56aff1444409e39fa164b0b4d4552a7b7a" @@ -2629,7 +2962,7 @@ as-array@^2.0.0: resolved "https://registry.yarnpkg.com/as-array/-/as-array-2.0.0.tgz#4f04805d87f8fce8e511bc2108f8e5e3a287d547" integrity sha1-TwSAXYf4/OjlEbwhCPjl46KH1Uc= -asn1@~0.2.3: +asn1@^0.2.4, asn1@~0.2.3: version "0.2.4" resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.4.tgz#8d2475dfab553bb33e77b54e59e880bb8ce23136" integrity sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg== @@ -3366,6 +3699,14 @@ chainsaw@~0.1.0: dependencies: traverse ">=0.3.0 <0.4" +chalk@*, chalk@^4.1.0, chalk@^4.1.1: + version "4.1.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" + integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + chalk@2.x, chalk@^2.0.0, chalk@^2.0.1, chalk@^2.3.0, chalk@^2.4.1, chalk@^2.4.2: version "2.4.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" @@ -3394,14 +3735,6 @@ chalk@^3.0.0: ansi-styles "^4.1.0" supports-color "^7.1.0" -chalk@^4.1.0, chalk@^4.1.1: - version "4.1.2" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" - integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== - dependencies: - ansi-styles "^4.1.0" - supports-color "^7.1.0" - chardet@^0.7.0: version "0.7.0" resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e" @@ -3568,7 +3901,7 @@ cli-progress@^3.7.0: colors "^1.1.2" string-width "^4.2.0" -cli-spinners@^2.0.0, cli-spinners@^2.5.0: +cli-spinners@^2.0.0, cli-spinners@^2.4.0, cli-spinners@^2.5.0: version "2.6.0" resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-2.6.0.tgz#36c7dc98fb6a9a76bd6238ec3f77e2425627e939" integrity sha512-t+4/y50K/+4xcCRosKkA7W4gTr1MySvLV0q+PxmG7FJ5g+66ChKurYjxBCjHggHH3HA5Hh9cy+lcUGWDqVH+4Q== @@ -3778,6 +4111,11 @@ combined-stream@^1.0.6, combined-stream@^1.0.8, combined-stream@~1.0.6: dependencies: delayed-stream "~1.0.0" +commander@6.2.1: + version "6.2.1" + resolved "https://registry.yarnpkg.com/commander/-/commander-6.2.1.tgz#0792eb682dfbc325999bb2b84fddddba110ac73c" + integrity sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA== + commander@^2.12.1, commander@^2.16.0, commander@^2.20.0, commander@^2.20.3, commander@^2.7.1, commander@^2.8.1: version "2.20.3" resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" @@ -6237,6 +6575,18 @@ gitconfiglocal@^1.0.0: dependencies: ini "^1.3.2" +github-app-installation-token@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/github-app-installation-token/-/github-app-installation-token-1.1.0.tgz#cac24c17d7c728d04ceee0bbf6146fb35d7ce7a7" + integrity sha512-ucpp+nEbm2wdqegf1Of7nhjzXVcDru0GCiZjQ3+jh/ixSGk7stOhRSgXFZLaZ9W0v58uc+H7UONCrMz3yRJtiw== + dependencies: + "@octokit/auth-app" "2.8.0" + "@octokit/rest" "18.0.12" + commander "6.2.1" + debug "4.3.1" + node-rsa "1.1.1" + ora "5.1.0" + glob-parent@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-3.1.0.tgz#9e6af6299d8d3bd2bd40430832bd113df906c5ae" @@ -6911,7 +7261,7 @@ humanize-ms@^1.2.1: dependencies: ms "^2.0.0" -husky@7.0.1: +husky@7.0.1, husky@^7.0.1: version "7.0.1" resolved "https://registry.yarnpkg.com/husky/-/husky-7.0.1.tgz#579f4180b5da4520263e8713cc832942b48e1f1c" integrity sha512-gceRaITVZ+cJH9sNHqx5tFwbzlLCVxtVZcusME8JYQ8Edy5mpGDOqD8QBCdMhpyo9a+JXddnujQ4rpY2Ff9SJA== @@ -7731,7 +8081,7 @@ jasmine@2.8.0: glob "^7.0.6" jasmine-core "~2.8.0" -jasmine@^3.5.0: +jasmine@^3.5.0, jasmine@^3.7.0: version "3.8.0" resolved "https://registry.yarnpkg.com/jasmine/-/jasmine-3.8.0.tgz#4497bc797eede7ca9de18179aedd4cf50245d8dc" integrity sha512-kdQ3SfcNpMbbMdgJPLyFe9IksixdnrgYaCJapP9sS0aLgdWdIZADNXEr+11Zafxm1VDfRSC5ZL4fzXT0bexzXw== @@ -8509,7 +8859,7 @@ log-symbols@^2.2.0: dependencies: chalk "^2.0.1" -log-symbols@^4.1.0: +log-symbols@^4.0.0, log-symbols@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.1.0.tgz#3fbdbb95b4683ac9fc785111e792e558d4abd503" integrity sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg== @@ -9322,6 +9672,13 @@ node-releases@^1.1.71: resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.73.tgz#dd4e81ddd5277ff846b80b52bb40c49edf7a7b20" integrity sha512-uW7fodD6pyW2FZNZnp/Z3hvWKeEW1Y8R1+1CnErE8cXFXzl5blBOoVB41CvMer6P6Q0S5FXDwcHgFd1Wj0U9zg== +node-rsa@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/node-rsa/-/node-rsa-1.1.1.tgz#efd9ad382097782f506153398496f79e4464434d" + integrity sha512-Jd4cvbJMryN21r5HgxQOpMEqv+ooke/korixNNK3mGqfGJmy0M77WDDzo/05969+OkMy3XW1UuZsSmW9KQm7Fw== + dependencies: + asn1 "^0.2.4" + node-source-walk@^4.0.0, node-source-walk@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/node-source-walk/-/node-source-walk-4.2.0.tgz#c2efe731ea8ba9c03c562aa0a9d984e54f27bc2c" @@ -9701,6 +10058,20 @@ optionator@^0.8.1: type-check "~0.3.2" word-wrap "~1.2.3" +ora@5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/ora/-/ora-5.1.0.tgz#b188cf8cd2d4d9b13fd25383bc3e5cba352c94f8" + integrity sha512-9tXIMPvjZ7hPTbk8DFq1f7Kow/HU/pQYB60JbNq+QnGwcyhWVZaQ4hM9zQDEsPxw/muLpgiHSaumUZxCAmod/w== + dependencies: + chalk "^4.1.0" + cli-cursor "^3.1.0" + cli-spinners "^2.4.0" + is-interactive "^1.0.0" + log-symbols "^4.0.0" + mute-stream "0.0.8" + strip-ansi "^6.0.0" + wcwidth "^1.0.1" + ora@5.4.0: version "5.4.0" resolved "https://registry.yarnpkg.com/ora/-/ora-5.4.0.tgz#42eda4855835b9cd14d33864c97a3c95a3f56bf4" @@ -10848,7 +11219,7 @@ prepend-http@^2.0.0: resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-2.0.0.tgz#e92434bfa5ea8c19f41cdfd401d741a3c819d897" integrity sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc= -prettier@^2.3.0: +prettier@^2.3.0, prettier@^2.3.2: version "2.3.2" resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.3.2.tgz#ef280a05ec253712e486233db5c6f23441e7342d" integrity sha512-lnJzDfJ66zkMy58OL5/NY5zp70S7Nz6KqcKkXYzn2tMVrNxvbqaBpg7H3qHaLxCJ5lNMsGuM8+ohS7cZrthdLQ== @@ -11359,7 +11730,7 @@ redeyed@~2.1.0: dependencies: esprima "~4.0.0" -reflect-metadata@^0.1.3: +reflect-metadata@^0.1.13, reflect-metadata@^0.1.3: version "0.1.13" resolved "https://registry.yarnpkg.com/reflect-metadata/-/reflect-metadata-0.1.13.tgz#67ae3ca57c972a2aa1642b10fe363fe32d49dc08" integrity sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg== @@ -11791,6 +12162,13 @@ rollup@2.16.1: optionalDependencies: fsevents "~2.1.2" +rollup@^2.53.3: + version "2.56.2" + resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.56.2.tgz#a045ff3f6af53ee009b5f5016ca3da0329e5470f" + integrity sha512-s8H00ZsRi29M2/lGdm1u8DJpJ9ML8SUOpVVBd33XNeEeL3NVaTiUcSBHzBdF3eAyR0l7VSpsuoVUGrRHq7aPwQ== + optionalDependencies: + fsevents "~2.3.2" + rollup@~1.11.3: version "1.11.3" resolved "https://registry.yarnpkg.com/rollup/-/rollup-1.11.3.tgz#6f436db2a2d6b63f808bf60ad01a177643dedb81" @@ -11901,7 +12279,6 @@ sauce-connect-launcher@^1.2.4: "sauce-connect@https://saucelabs.com/downloads/sc-4.6.2-linux.tar.gz": version "0.0.0" - uid "7b7f35433af9c3380758e048894d7b9aecf3754e" resolved "https://saucelabs.com/downloads/sc-4.6.2-linux.tar.gz#7b7f35433af9c3380758e048894d7b9aecf3754e" saucelabs@^1.5.0: @@ -13248,15 +13625,20 @@ tslib@2.2.0: resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.2.0.tgz#fb2c475977e35e241311ede2693cee1ec6698f5c" integrity sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w== +tslib@2.3.0, tslib@^2.0.1, tslib@^2.2.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.0.tgz#803b8cdab3e12ba581a4ca41c8839bbb0dacb09e" + integrity sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg== + tslib@^1.10.0, tslib@^1.13.0, tslib@^1.8.1, tslib@^1.9.0: version "1.14.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== -tslib@^2.0.1, tslib@^2.2.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.0.tgz#803b8cdab3e12ba581a4ca41c8839bbb0dacb09e" - integrity sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg== +tslib@^2.0.0, tslib@^2.3.0: + version "2.3.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.1.tgz#e8a335add5ceae51aa261d32a490158ef042ef01" + integrity sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw== tslib@~2.1.0: version "2.1.0" @@ -13277,7 +13659,7 @@ tslint-no-toplevel-property-access@0.0.2: resolved "https://registry.yarnpkg.com/tslint-no-toplevel-property-access/-/tslint-no-toplevel-property-access-0.0.2.tgz#c9b19bbd525ea7b8577e5ada601cc8625b4ed004" integrity sha512-Oc+UUurlGLBkgeUSGxMoTpRUpaXsjqzQCEAYrYQyuU8330fi5FKlye5n53y87EJ24AlfdoxMPV7DJfFOADapfg== -tslint@6.1.3: +tslint@6.1.3, tslint@^6.1.3: version "6.1.3" resolved "https://registry.yarnpkg.com/tslint/-/tslint-6.1.3.tgz#5c23b2eccc32487d5523bd3a470e9aa31789d904" integrity sha512-IbR4nkT96EQOvKE2PW/djGz8iGNeJ4rF2mBfiYaR/nvUWYKJhLwimoJKgjIFEIDibBtOevj7BqCRL4oHeWWUCg== @@ -13324,6 +13706,11 @@ tunnel-agent@^0.6.0: dependencies: safe-buffer "^5.0.1" +tunnel@0.0.6: + version "0.0.6" + resolved "https://registry.yarnpkg.com/tunnel/-/tunnel-0.0.6.tgz#72f1314b34a5b192db012324df2cc587ca47f92c" + integrity sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg== + tweetnacl@^0.14.3, tweetnacl@~0.14.0: version "0.14.5" resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" @@ -13424,6 +13811,11 @@ typescript@4.2.4: resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.2.4.tgz#8610b59747de028fda898a8aef0e103f156d0961" integrity sha512-V+evlYHZnQkaz8TRBuxTA92yZBPotr5H+WhQ7bD3hZUndx5tGOa1fuCgeSjxAzM1RiN5IzvadIXTVefuuwZCRg== +typescript@4.3.4: + version "4.3.4" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.3.4.tgz#3f85b986945bcf31071decdd96cf8bfa65f9dcbc" + integrity sha512-uauPG7XZn9F/mo+7MrsRjyvbxFpzemRjKEZXS4AK83oP2KKOJPvb+9cO/gmnv8arWZvhnjVOXz7B49m1l0e9Ew== + typescript@^3.9.5, typescript@^3.9.7: version "3.9.10" resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.10.tgz#70f3910ac7a51ed6bef79da7800690b19bf778b8" @@ -13571,6 +13963,14 @@ universal-analytics@^0.4.16: request "^2.88.2" uuid "^3.0.0" +universal-github-app-jwt@^1.0.1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/universal-github-app-jwt/-/universal-github-app-jwt-1.1.0.tgz#0abaa876101cdf1d3e4c546be2768841c0c1b514" + integrity sha512-3b+ocAjjz4JTyqaOT+NNBd5BtTuvJTxWElIoeHSVelUV9J3Jp7avmQTdLKCaoqi/5Ox2o/q+VK19TJ233rVXVQ== + dependencies: + "@types/jsonwebtoken" "^8.3.3" + jsonwebtoken "^8.5.1" + universal-user-agent@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/universal-user-agent/-/universal-user-agent-6.0.0.tgz#3381f8503b251c0d9cd21bc1de939ec9df5480ee"