Compare commits

..

108 Commits

Author SHA1 Message Date
Dylan Hunn b31f5dd2f0 release: cut the v14.1.2 release 2022-08-10 11:07:49 -07:00
Andrew Scott 2548712aba build: add atscott to unavailable users (#47102)
PR Close #47102
2022-08-10 10:45:38 -07:00
4javier f4fa4afd04 docs: clarify providers' injection in lazy loaded modules (#47094)
Addresses https://github.com/angular/angular/issues/47059
I try to keep the edit as concise as possible because adding any further
explanation would bring to much deeper info compared to the topic of the
page

Removed some convoluted info and replaced with a link to a page that
should focus on the specific aspect of the matter.
Conditional is obliged because IMHO that page should have a dedicated
paragraph explaining various ways of populating different injectors with
providers, and maybe integrating the interesting parts from the recent
created page about standalone app.
PR Close #47094
2022-08-10 10:45:04 -07:00
Andrew Scott 439d77e852 fix(router): Fix route recognition behavior with some versions of rxjs (#47098) (#47112)
Some versions of rxjs cause the algorithm used in the Router to not recognize Route configs correctly.
This commit updates the algorithm to be compatible in the same way as other code locations internally.

Context:
https://github.com/angular/angular/commit/1160b8194f78141b40d1d4885ec182d033ff2659

fixes #47089

Note: This does not have a test because I was unable to identify the
version of rxjs that would cause a failure here.

PR Close #47098

PR Close #47112
2022-08-10 10:43:50 -07:00
Angular Robot 036f740f77 build: update github/codeql-action action to v2.1.18 (#47015)
See associated pull request for more information.

PR Close #47015
2022-08-09 10:56:00 -07:00
AntonioCardenas e10aa8858f docs: add antoniocardenas to GDE resources (#47033)
PR Close #47033
2022-08-09 10:02:12 -07:00
Vovch 38d23af419 docs: change "zone" to "ngZone" in an example (#47079)
Change requesting "this.zone" to "this.ngZone" in the Plotly example, as it corresponds to the name declared in the constructor.
PR Close #47079
2022-08-09 10:01:27 -07:00
Angular Robot 923a7446a2 build: update eslint dependencies to v5.33.0 (#47087)
See associated pull request for more information.

PR Close #47087
2022-08-09 10:00:11 -07:00
Bob Watson 874d78b502 docs: add more documentation about lint messages (#47019)
Adds suggestions for how to resolve the most common messages produced by the documentation linter.

Adds links to the Proselint and other style tests to the new documentation.

Updates pullapprove and navigation.

PR Close #47019
2022-08-08 11:34:56 -07:00
onrails 8d18969714 docs: updated reference link to NativeScript/Angular documentation (#47063)
PR Close #47063
2022-08-08 11:34:21 -07:00
Mladen Jakovljević 2106b74853 refactor: improve disabled attribute warning (#47041)
Users using the "disabled" property binding on reactive form controls would want to know how to dynamically update the disabled state of a form control when they get a console warning.

PR Close #47041
2022-08-08 11:33:53 -07:00
Eduardo Speroni 5ff715c549 fix(core): check if transplanted views are attached to change detector (#46974)
Prevents change detection on views transplanted in OnPush components that have been detached from change detection.

PR Close #46974
2022-08-08 11:33:29 -07:00
Edward 30c9e80a30 docs: fix dependecy injection documentation page typos (#47057)
PR Close #47057
2022-08-08 11:33:05 -07:00
Bruno Barbosa 0392feb135 fix(docs-infra): fix nav css on tutorial tour of heroes (#47064)
Fix the navigation button to work properly by the css style applied when the button is active, on tutorial Tour of Heroes.

PR Close #47064
2022-08-08 11:32:40 -07:00
Kristiyan Kostadinov 6d215d3124 refactor(core): remove unused module tracking (#47066)
We used to track all modules in a top-level constant called `autoRegisterModuleById` which was used by `getRegisteredNgModuleType`. As of #45024 the constant isn't being used anymore so we can remove it.

PR Close #47066
2022-08-08 11:32:17 -07:00
Roman Matusevich e46972ac4b docs: fix text formatting for paragraph in Hierarchical injectors (#47067)
Paragraph of text is not wrapped into `p` tag due to missing empty line after the `div` block. The fix is just add an empty line.

PR Close #47067
2022-08-08 11:31:44 -07:00
Pawel Kozlowski 2e42bb9c62 docs: update link to the TypeScript handbook (#47075)
Closes #47070
PR Close #47075
2022-08-08 11:31:19 -07:00
Paul Gschwendtner d14235cfb1 ci: invalidate circleci caches to prune nested node_modules (#46707)
Yarn 1.x. is known to not prune nested unused node modules. This throws
off Bazel when the dependency tree changes but there are leftover unused
nested node module folders. This causes CI failures currently after the
dependency tree updates.

Invalidating the cache fixes this issue. Long-term a switch to Yarn
2.x+, pnpm will fix this.

PR Close #46707
2022-08-08 09:23:56 -07:00
Angular Robot befd650a78 build: update all non-major dependencies (#46707)
See associated pull request for more information.

PR Close #46707
2022-08-08 09:23:56 -07:00
Jan Melcher fd2abebf91 docs: fix link in dependency injection guide (#47055)
PR Close #47055
2022-08-08 09:23:06 -07:00
ileil 387f4a79f8 docs: update dependency injection content (#44466)
Refactored the DI section to improve doc quality, reduce cognitive load and drive consistency.

- Added an overview with prerequisites and doc cards that point to rest of the DI content
- Added introduction topic with topic purpose, value proposition and "fail fast"
- Broke apart complex concepts into simpler tasks
- Unified tone and language for each topic
- Added new content based on SME feedback
- Deleted obsolete content

PR Close #44466
2022-08-05 10:06:14 -07:00
Angular Robot 8f32c09872 build: update io_bazel_rules_sass digest to 697e5a0 (#47046)
See associated pull request for more information.

PR Close #47046
2022-08-05 09:57:45 -07:00
Paul Gschwendtner 04b3497db1 build: update ngsw-config.json after redirect changes (#47048)
Looks like the `ngsw-config.json` file needs to be updated after a new
redirect being added in: https://github.com/angular/angular/pull/46820.

PR Close #47048
2022-08-05 09:56:57 -07:00
Paul Gschwendtner d0e3ad5300 refactor(docs-infra): update safevalues and account for API changes (#47048)
Updates `safevalues` to unblock
https://github.com/angular/angular/pull/46707. As part of this commit
the necessary refactorings due to API changes are also made.

PR Close #47048
2022-08-05 09:56:57 -07:00
Andrew Scott c7fed38cf0 refactor(router): Cherry-pick Router services to be providedIn: 'root' (#47052)
Recent refactoring in the Router made services available via
`providedIn: 'root'`. However, some of these changes were not merged to
the patch branch. This commit updates the remaining services needed to
create the `Router` without `RouterModule` and adds a test to ensure it
works.

PR Close #47052
2022-08-05 09:36:11 -07:00
Angular Robot 1d39ab6e6b build: update io_bazel_rules_sass digest to 70ffe01 (#47023)
See associated pull request for more information.

PR Close #47023
2022-08-04 13:08:42 -07:00
ashide2729 fce4ea1d8d docs: add yarn build fail workaround on windows (#47037)
PR Close #47037
2022-08-04 13:08:19 -07:00
George Kalpakas 2a833e6eac ci: fix Renovate post-upgrade task command (#47040)
Since Renovate runs from the project directory, it cannot find the
`sync-deps` yarn script inside `aio/tools/examples/shared/`:
[example failure][1]

Update the post-upgrade task command to run inside that directory
instead of the project root.

[1]: https://github.com/angular/angular/pull/46707#issuecomment-1203335639

PR Close #47040
2022-08-04 13:07:56 -07:00
Bob Watson d0f4f1d601 docs: update Vale installation instructions (#47031)
This update includes:
* installation directions for the current version of the Vale VSCode extension (v. 0.15.0)
* a workaround for if the current version of the extension doesn't work.

PR Close #47031
2022-08-03 17:35:31 -07:00
Andrew Kushnir 3ee883e417 fix(docs-infra): do not require @ngModule tag on standalone types (#47024)
This commit updates docs extraction logic to avoid requiring the `@ngModule` tag on standalone types, since they don't have to be present in any NgModule.

PR Close #47024
2022-08-03 11:04:56 -07:00
Andrew Kushnir b868f1c94e release: cut the v14.1.1 release 2022-08-03 10:48:18 -07:00
Andrew Scott efb8e3e431 docs(core): Update inject error documentation (#47020)
The inject function is also available when using EnvironmentInjector.runInContext.

PR Close #47020
2022-08-02 18:06:55 -07:00
Angular Robot 8085fe34f1 build: update eslint dependencies to v5.32.0 (#47016)
See associated pull request for more information.

PR Close #47016
2022-08-02 11:59:42 -07:00
George Kalpakas 9edfe4ac97 build(docs-infra): ensure all boilerplate dependencies are in sync (#47009)
All docs examples share the same `node_modules/` (symlinked into each
example from `aio/tools/examples/shared/node_modules/`). However, each
example type has a different `package.json`, which comes from
`aio/tools/examples/shared/boilerplate/*`). In order to ensure that the
dependencies in each example's `package.json` are the same as the ones
in the symlinked `node_modules/` (i.e. the ones that CI tests are run
with), we have a script (`yarn run sync-deps`) that can sync
dependencies from `shared/package.json` into the boilerplate
`package.json` files.

Previously, this script had to be run manually, which was easy to
forget/not know about and resulted in the boilerplate dependencies
often being out-of-sync with the ones in `shared/package.json` (and by
extension, the ones that were actually installed in `node_modules/`).

This commit helps keep the boilerplate dependencies up-to-date in the
following ways:
- Adds the `sync-deps` script to the `postinstall` scripts.
  This ensures that dependencies remain in sync whenever someone
  manually updates dependencies in `shared/package.json`.
- Runs the `sync-deps` script as a Renovate post-upgrade task.
  This ensures that the depenencies remain in sync whenever Renovate
  updates dependencies in `shared/package.json`.
  For more info on configuring post-upgrade tasks in Renovate, see:
  - [postUpgradeTasks][1]
  - [allowedPostUpgradeCommands][2]
  - [allowPostUpgradeCommandTemplating][3]

NOTE:
For the Renovate change to take effect, the [global config][4] in
`angular/dev-infra` also needs to be updated. This will be done in a
separate PR.

[1]: https://docs.renovatebot.com/configuration-options/#postupgradetasks
[2]: https://docs.renovatebot.com/self-hosted-configuration/#allowedpostupgradecommands
[3]: https://docs.renovatebot.com/self-hosted-configuration/#allowpostupgradecommandtemplating
[4]: https://github.com/angular/dev-infra/blob/22d3067021130271afcfd02d063828c5bdd9c2d7/.github/ng-renovate/runner-config.js

PR Close #47009
2022-08-02 11:58:51 -07:00
Andrew Scott 79825d3f10 fix(router): Do not call preload method when not necessary (#47007)
In Angular 14, we introduced the `loadComponent` API for a `Route` to
allow lazy loading of a routed component in addition to the existing
`loadChildren` which allows lazy loading of child routes. As a result,
the `preload` method of the `PreloadingStrategy` needs to sometimes be
called even when there is a `canLoad` guard on the `Route`. `CanLoad`
guards block loading of child routes but _do not_ block loading of the
component.

This change updates the conditional checks in the internal preloader to
skip calling the `PreloadingStrategy.preload` when there is only a
`loadChildren` callback with a `canLoad` guard an no `loadComponent`.
In this case, the callback passed to the `preload` method is already
effectively a no-op so it's not necessary to call it at all.

resolves #47003

PR Close #47007
2022-08-02 09:38:28 -07:00
Paul Gschwendtner 4e9492cf6d build: update dev-infra packages and account for build-tooling split from ng-dev (#46976)
The dev-infra build tooling is now decoupled from `ng-dev`. This will
make it easier to update `ng-dev` without necessarily needing to upgrade
the whole build system, Bazel etc. This is useful when e.g. new release
tool features have been added and should also be ported to active LTS
branches.

PR Close #46976
2022-08-02 09:37:38 -07:00
Andrew Scott 3f162f5e3f refactor(common): Align PathLocationStrategy constructor with default factory (#46929)
When using the Angular Router, one of `APP_BASE_HREF` or a `<base>` in
the header must be provided. When _not_ using the `RouterModule`,
injecting the `LocationStrategy` will result in the
`PathLocationStrategy` being provided with a default value used in place
of `APP_BASE_HREF` that is `document?.location?.origin ?? ''`.

It can be quite surprising and annoying that once you add `RouterModule`
to the application, suddenly the `APP_BASE_HREF` must be specifically
provide something new when it could use a sensible default instead.

The current behavior (before this commit) is as follows:

* When `RouterModule` is not provided (or the dev doesn't specifically provide
  `PathLocationStrategy`): use `DOCUMENT.location?.origin ?? ''`.
  Note that the base href in the dom and `APP_BASE_HREF` are not used.
* When `RouterModule` _is_ provided:
   1. APP_BASE_HREF if defined
   2. Get base href from DOM
   3. throw if neither of the two above are defined

This commit updates this behavior to be aligned regardless of `RouterModule`
usage. The order (by default) is now:
  1. Developer provided `APP_BASE_HREF`
  2. base href from the DOM
  3. `location.origin`
  4. If none of the above exist, use `''`

This is slightly different than the behavior before. However, I believe
it is more appropriate. For the case without `RouterModule`, it would
likely be surprising that `APP_BASE_HREF` and the base href from the DOM
are ignored by default. For the case with `RouterModule`, we now have a
more sensible fallback/default when neither `APP_BASE_HREF` nor `<base>`
are defined (instead of just throwing an error).

PR Close #46929
2022-08-01 14:21:14 -07:00
Marek Hám b6445c4cd6 docs: fix link to live example (#47011)
Fixes #46975

PR Close #47011
2022-08-01 14:20:37 -07:00
Durairaj Subramaniam d05d05e48e docs: fix link in routing (#46988)
PR Close #46988
2022-08-01 13:00:25 -07:00
Angular Robot 7a7452dc28 build: update bazel setup to v5.5.3 (#47000)
See associated pull request for more information.

PR Close #47000
2022-08-01 12:55:44 -07:00
Andrew Scott 05f3f7445a fix(router): Use correct return type for provideRoutes function (#46941)
The provideRoutes function of the Router returns a Provider array and should not be typed as 'any'

PR Close #46941
2022-08-01 11:20:50 -07:00
Andrew Scott 6307f3586e refactor(router): Remove unused ANALYZE_FOR_ENTRY_COMPONENTS (#46942)
`entryComponents` is a feature that is not used or necessary in Angular
anymore.

PR Close #46942
2022-08-01 11:18:10 -07:00
Roman Matusevich ce6ad5a66a docs: fix selector in Component Interaction guide (#46995)
The selector for the `CountdownTimerComponent` is `app-countdown-timer`
not `countdown-timer`.

PR Close #46995
2022-08-01 11:16:31 -07:00
Asaf M cbfd91b0f2 docs: fixed typos (#47008)
Used my script: https://github.com/Uziel302/websites-spell-checker/blob/master/mdnversion

PR Close #47008
2022-08-01 11:15:55 -07:00
J Rob Gant 8cd95d4901 refactor(zone.js): remove leftover debugging code using Error.stack (#46989)
Pull request #46672 added some debugging code to trace down the
root cause of its bug, but parts of the debugging code has never
been cleaned up and ended up landing as part of the PR.

This commit removes the code as it might cause unexpected issues.
Likely when e.g. `Error` is patched and would perform XHRs in testing,
unveiling e.g. CORS issues. See #46989.

PR Close #46989
2022-08-01 09:52:30 -07:00
Cédric Exbrayat ec00075a2f refactor(compiler-cli): remove unused error codes (#46847)
A few codes were unused, or no longer used.

PR Close #46847
2022-08-01 09:50:12 -07:00
Kristiyan Kostadinov a0a635322c fix(bazel): allow extendedDiagnostics option to be passed in through tsconfig (#46953)
Adds the `extendedDiagnostics` field to the list of allowed options so that it is picked up from the user's tsconfig.

PR Close #46953
2022-08-01 09:49:14 -07:00
Angular Robot 8dcd3cd263 build: update eslint dependencies to v5.31.0 (#46968)
See associated pull request for more information.

PR Close #46968
2022-08-01 09:46:41 -07:00
Angular Robot 677fe6a1cc build: update io_bazel_rules_sass digest to 030f696 (#46947)
See associated pull request for more information.

PR Close #46947
2022-08-01 09:40:03 -07:00
Angular Robot 5d8394cc8c build: update dependency google-closure-compiler to v20220719 (#46926)
See associated pull request for more information.

PR Close #46926
2022-08-01 09:39:26 -07:00
Angular Robot acdb28dc1d build: update cross-repo angular dependencies to 6d9c069 (#46905)
See associated pull request for more information.

PR Close #46905
2022-08-01 09:38:35 -07:00
Aristeidis Bampakos c5a5165294 docs: modify callout in TOH part 2 (#46964)
PR Close #46964
2022-08-01 09:30:04 -07:00
Andrew Quinn 71831c63ba docs: fix typo in guide/structural-directives page (#46961)
Fix typo [missing plural marker "element_s_"] on https://angular.io/guide/structural-directives.

Original:
>Structural directives are directives which change the DOM layout by adding and removing DOM element.

Fix:
>Structural directives are directives which change the DOM layout by adding and removing DOM element**s**.
PR Close #46961
2022-08-01 09:29:40 -07:00
Bob Watson 3511373e22 docs: initial upload of styles used by doc linter (#46897)
Add the configuration and style files to support documentation linting. The `README.md` file includes instructions for installation and use of the lint tool.

PR Close #46897
2022-08-01 09:29:06 -07:00
Bob Watson ff7c71653a build: update doc approvers (#46939)
Remove: TMDavis, who is no longer at Google
Add: bob-watson who is now on the team

PR Close #46939
2022-07-22 15:39:34 +00:00
Ivaylo Kirov 6b779b11d1 docs: add missing period to the Security guide (#46900)
PR Close #46900
2022-07-22 15:37:30 +00:00
Simona Cotin 1eb77a2f4f docs: add simona cotin to contributors (#46922)
PR Close #46922
2022-07-22 08:27:08 +00:00
Alan Agius 929d69e62f docs(router): remove incorrect deprecation text in InitialNavigation (#46916)
Remove left over deprecation note.

PR Close #46916
2022-07-22 08:26:25 +00:00
Andrew Scott f5d764d75c refactor(router): Update internal INITIAL_NAVIGATION token to use an enum (#46925)
This updates the internal use of INITIAL_NAVIGATION to do two things:

1. Explicitly provide `Enabled` as the default for the token factory
2. Use an enum instead of a string to reduce bundle size

PR Close #46925
2022-07-22 08:25:37 +00:00
Andrew Kushnir 55f485b3bb refactor(platform-server): include TransferState providers into ServerModule (#46899)
This commit updates the code to include the TransferState providers (used for serialization) into the `ServerModule` instead of having the need to import the `ServerTransferStateModule` separately.

The list of providers in the `ServerTransferStateModule` is now empty and importing it is a noop. This is not a breaking change, since the `ServerModule` must be included anyways to make server rendering work correctly.

PR Close #46899
2022-07-21 12:17:43 +00:00
Cédric Exbrayat 3606917732 fix(core): improve the missing control flow directive message (#46903)
Similarly to what has been done in #46846 for the extended diagnostics about missing control flow directive that was only mentioning that the `CommonModule` should be imported, this commit improves the validation done by the JiT compiler.
Now that the control flow directives are available as standalone, the message mentions that directive itself can be imported.

The message now also mentions which import should be used for the directive (as it can be tricky to figure out that `NgForOf` is the directive corresponding to `*ngFor`).

PR Close #46903
2022-07-21 08:27:32 +00:00
Andrew Scott 0cbbd6aeec refactor(router): Update Router to be providedIn: 'root' (#46914)
This commit updates the Router itself to be `providedIn: 'root'` with a
factory function rather than provided in the `RouterModule`.

PR Close #46914
2022-07-20 12:21:05 -07:00
Andrew Kushnir a8e9247e6a refactor(platform-server): make TransferState standalone-friendly (#46879)
This commit updates the `TransferState` to make it `providedIn: 'root'`. This makes the entire `BrowserTransferStateModule` module unnecessary, so it got deprecated as well.

The `ServerTransferStateModule` is still retained, but the `renderApplication` function now also includes the necessary tokens to serialize the `TransferState` automatically, so when using the `renderApplication` function, there is no need to include `ServerTransferStateModule` as well.

This change is a part of the ongoing efforts to update the shape of the FW APIs to make them standalone-friendly (so there is no need to import any NgModules).

PR Close #46879
2022-07-20 11:05:08 -07:00
Andrew Kushnir 793326deef release: cut the v14.1.0 release 2022-07-20 10:44:59 -07:00
Doug Parker 10d46b4247 refactor(bazel): remove experimental_extended_template_diagnostics flag (#46898)
Extended diagnostics are enabled by default now and this flag doesn't do anything anymore but I missed it in a previous cleanup.

PR Close #46898
2022-07-20 08:50:45 -07:00
dario-piotrowicz b744bf6a08 refactor(compiler-cli): improve error for non-exported non-standalone (#46114)
improve the error message for non-standalone components which are not
exported from their module, and that are also imported directly as if
they were standalone

this change simply adds the suggestion to the developer to import the
ngModule instead

resolves #46004

PR Close #46114
2022-07-20 08:50:01 -07:00
Andrew Kushnir 2e5bd88ebf refactor(core): NgModuleRef should not implement EnvironmentInjector interface (#46896)
This commit refactors the `NgModuleRef` implementation to drop functions required by the `EnvironmentInjector` interface. Previously the idea was that the `NgModuleRef` can act as an Injector to facilitate easier transition to standalone. However, from the mental model perspective, the `NgModuleRef` has the `injector` field, which is the correct injector reference and can be used is needed as an `EnvironmentInjector`.

PR Close #46896
2022-07-20 08:49:15 -07:00
Andrew Scott 9b8ea9b5ca Revert "fix(core): Fix runInContext for NgModuleRef injector (#46877)" (#46896)
This reverts commit 14081dc48d.

PR Close #46896
2022-07-20 08:49:15 -07:00
Angular Robot eb91035102 build: update cross-repo angular dependencies to cdaaa15 (#46853)
See associated pull request for more information.

PR Close #46853
2022-07-20 08:48:45 -07:00
Bob Watson 17e2a2a9d1 docs: add info about request timeouts (#46882)
* Adds a short note about how service workers report request timeouts
* Updated heading levels to present a clearer hierarchy and cleaner local TOC
* Removed documentation lint errors
* Moves the service worker service timeout paragraph to a more logical topic in service-worker-devops
* Removes line error from the more logical topic.
* address feedback from PR review comments

fixes #46445

PR Close #46882
2022-07-20 08:48:00 -07:00
Paul Gschwendtner b431b5c6d6 test: convert source-map core tests to use async/await instead of fakeAsync (#46888)
The source map tests rely on asynchronous logic from the `source-map`
package. The tests itself are written using `fakeAsync` but this
unnecessarily complicates the interaction with the asynchronous
source-map helpers/package.

To fix this, we just make the tests use async/await as we don't intend
to test fakeAsync in this `describe` block..

PR Close #46888
2022-07-19 09:40:57 -07:00
Paul Gschwendtner bc371dac44 build: disable jasminewd2 types for sourcemap playground test (#46888)
The sourcemap test in the e2e playground is now using async/await code.
This results in errors now with the Bazel TS compilation because it
detects that `expect` is returning a promise and should be awaited.

This happens due to the jasminewd2 types. We should just use the actual
jasmine types and not rely on the deprecated selenium control flow,
using explicit async/await in the whole test. This also solves the issue
with the source-map types being async/await now.

PR Close #46888
2022-07-19 09:40:57 -07:00
Paul Gschwendtner 36361b0506 test: update source-map tests to account for source-map breaking change (#46888)
The source-map package now requires the
`SourceMapConsumer`/`SourceMapGenerator` classes to be instantiated
asynchronously. This commit updates our tests to account for that.

PR Close #46888
2022-07-19 09:40:57 -07:00
Paul Gschwendtner 38f53a55c3 build: remove source-map package from compiler-cli bundling external list (#46888)
The source-map package is no longer explicitly used in the
`compiler-cli` package and therefore can be removed from the ESBuild
bundling, denoting it as external. This should be a noop.

PR Close #46888
2022-07-19 09:40:56 -07:00
Paul Gschwendtner 8de3d099d2 build: update source-map package to latest version (#46888)
The source-map package is updated as part #46707, but the minor
version bump introduced breaking changes. This commit extracts the
update so that we can handle it separately accounting for the breaking
changes. Also removing the caret since this package apparently makes
breaking changes in minor segment bumps.

PR Close #46888
2022-07-19 09:40:56 -07:00
Andrew Scott 945a3ad359 fix(core): Fix runInContext for NgModuleRef injector (#46877)
The `runInContext` for `NgModuleRef` was previously an infinite loop.

PR Close #46877
2022-07-19 09:37:32 -07:00
Angular Robot 88003e27db build: update github/codeql-action action to v2.1.16 (#46885)
See associated pull request for more information.

PR Close #46885
2022-07-19 16:05:06 +00:00
Angular Robot a4cba79440 build: update eslint dependencies to v5.30.7 (#46884)
See associated pull request for more information.

PR Close #46884
2022-07-19 16:04:37 +00:00
Angular Robot 6ca7a79bfd build: update babel dependencies to v7.18.9 (#46883)
See associated pull request for more information.

PR Close #46883
2022-07-19 16:04:09 +00:00
Andrew Scott e95ee96562 refactor(router): Update TitleStrategy to useFactory (#46876)
The implementation of the `DefaultTitleStrategy` was modeled after the
existing strategy patterns in the Router. These patterns were developed
before the `providedIn` syntax for injectables. We can simplify the
model a lot by providing the default in the factory of the abstract
class.

Note that the other strategy patterns aren't touched in this PR due to
how long they've existed. Because they have been there for such a long
time, it's possible there will need to be some adjustments to code
if/when they are refactored to do the same.

PR Close #46876
2022-07-18 22:02:19 +00:00
dario-piotrowicz 536ded2280 fix(animations): make sure falsy values are added to _globalTimelineStyles (#46863)
style values get added to the `_globalTimelineStyles` map in order to keep
them so that they can be used across different timelines

`_globalTimelineStyles` was previously a plain object but has been
refactored to a map in #44482, as part of the update a check has been
changed from a ternary operation to an or (||), causing falsy values (as 0)
not to be added to the map anymore, apply the nullish coalescing operator (??)
instead to make sure only `undefined` and `null` are filtered out

also since this aspect was clearly not covered by tests, add a new test
to ensure that such regression doesn't happen in the future

resolves #46833

PR Close #46863
2022-07-18 21:46:13 +00:00
Cédric Exbrayat 6b8e60c06a fix(compiler-cli): improve the missingControlFlowDirective message (#46846)
The extended diagnostics about missing control flow directive was only mentioning that the `CommonModule` should be imported.
Now that the control flow directives are available as standalone, the message mentions that directive itself can be imported.

The message now also mentions which import should be used for the directive (as it can be tricky to figure out that `NgForOf` is the directive corresponding to `*ngFor`).

PR Close #46846
2022-07-18 19:56:08 +00:00
Paul Gschwendtner 5004b6ebf1 test: update tests to not run jasmine done function in sync-test zone from describe block
There are some ZoneJS tests that fork the zone from the `describe` block
for testing the zone patching. This does cause the Jasmine `done`
function later in `it` specs to be invoked in the sync-test zone from
the original `describe` block. The `done` implementation now has
changed with the Karma Jasmine update and breaks because it now causes
tasks to be scheduled.

It is conceptually incorrect/invalid to take the describe sync zone and
run test logic with that sync zone.

```
 An error was thrown in afterAll
  error properties: Object({ originalStack: 'Error: Cannot call jasmine.execute().forceTask from within a sync test (syncTestZone for jasmine.describe#FileReader).
      at new ZoneAwareError (packages/zone.js/test/browser_test_rollup.umd.js:98:37)
      at e.onScheduleTask (packages/zone.js/bundles/zone-testing-bundle.umd.min.js:158:196)
      at e.scheduleTask (packages/zone.js/bundles/zone-testing-bundle.umd.min.js:14:7529)
      at t.scheduleTask (packages/zone.js/bundles/zone-testing-bundle.umd.min.js:14:3539)
      at t.scheduleMicroTask (packages/zone.js/bundles/zone-testing-bundle.umd.min.js:14:3791)
      at r.execute (packages/zone.js/bundles/zone-testing-bundle.umd.min.js:166:4312)
      at queueRunnerFa ...
      at <Jasmine>
```
2022-07-18 17:19:30 +00:00
Paul Gschwendtner 6c08635662 feat(zone.js): include jasmine describe block name when raising unexpected task error
As mentioned in the previous commit that ensured that the Zone name is
included in errors raised by the `SyncTestZoneSpec`, we can now include
the Jasmine describe block descriptions in such errors.

Errors can often happen when users accidentally try to set up Angular
tests without an `it` block. Resulting in errors where it's not clear
at all which describe block (of potentially a large repository) is
involved:

```
 An error was thrown in afterAll
  error properties: Object({ originalStack: 'Error: Cannot call XX from within a sync test.
      at new ZoneAwareError (packages/zone.js/test/browser_test_rollup.umd.js:98:37)
      at e.onScheduleTask (packages/zone.js/bundles/zone-testing-bundle.umd.min.js:158:196)
      at e.scheduleTask (packages/zone.js/bundles/zone-testing-bundle.umd.min.js:14:7529)
      at t.scheduleTask (packages/zone.js/bundles/zone-testing-bundle.umd.min.js:14:3539)
      at t.scheduleMicroTask (packages/zone.js/bundles/zone-testing-bundle.umd.min.js:14:3791)
      at r.execute (packages/zone.js/bundles/zone-testing-bundle.umd.min.js:166:4372)
      at queueRunnerFa ...
      at <Jasmine>
```

We now include the describe block description in the error, so that it
is easier to figure out the location of the culprit code.
2022-07-18 17:19:30 +00:00
Paul Gschwendtner f0add81caf feat(zone.js): include zone name when sync-test zone reports tasks
The sync-test zone is used in e.g. `describe` to raise an error when
there is asynchronous code scheduled in describe blocks. This commit
includes the zone name in such thrown errors to allow for us to include
the jasmine describe name in the error. This will be wired up in the
jasmine zonejs patches separately.
2022-07-18 17:18:49 +00:00
Paul Gschwendtner 21e7584c8c test: fix fake-async-test spec to properly test prefixed animation frame functions
In the fake async test for `zone.js` there is some logic to test various
prefixes of the request animation frame functions. This logic does not
have any effect currently, except for generating the same test at
maximum three times.

This commit fixes the test to actually test what it meant to do
initially.
2022-07-18 17:18:49 +00:00
Paul Gschwendtner 92c9661165 test: update tests to account for karma-jasmine v5.0.0
Karma jasmine updated the `jasmine-core` dependency. Jasmine is now more
strict when:

* The done callback is invoked multiple times
* The done callback is used, while a promise is also returned
* The done callback is treated as error when e.g. a number is returned
  as first argument. This was the case with `requestAnimationFrame`.
2022-07-18 17:18:49 +00:00
Paul Gschwendtner a462f91d3e fix(core): do not invoke jasmine done callback multiple times with waitForAsync
Currently tests written using `waitForAsync` would be prone to Jasmine
warnings or errors (depending on the version) for tests incorrectly
invoking asynchronous jasmine `done` callbacks multiple times.

This can happen because the async test zone logic schedules the
`done` callback to be called using `setTimeout`, but this could
be invoked multiple times, causing multiple `done` invocations to
be scheduled. Most of the issues have been resolved with #45025,
but it does not solve the case of multiple tasks finished and callbacks
being scheduled.

Technically, the current logic is built in way that _should_ result in
`_finishCallbackIfDone` and eventually the `done` callback to be invoked
at maximium once. This is unfortunately not the case in some rather
advanced/unexpected scenarios (like our AngularJS upgrade tests) where
the scenario is the following (and microtasks from before the actual
`waitForAsync` spec are still completing -- which is valid):

```
1. A test `beforeEach` schedules a microtask in the ProxyZone.
2. An actual empty `it` spec executes in the AsyncTestZone` (using e.g. `waitForAsync`).
3. The `onInvoke` invokes `_finishCallbackIfDone` because the spec runs synchronously.
4. We wait the scheduled timeout (see below) to account for unhandled promises.
5. The microtask from (1) finishes and `onHasTask` is invoked.

--> We register a second `_finishCallbackIfDone` even though we have scheduled a timeout.
--> we execute the `done` callback twice because the async zone spec state is "stable"
```
2022-07-18 17:18:49 +00:00
Angular Robot aa8d3ed69b build: update dependency karma-jasmine to v5
| datasource | package       | from  | to    |
| ---------- | ------------- | ----- | ----- |
| npm        | karma-jasmine | 4.0.2 | 5.1.0 |
2022-07-18 17:18:49 +00:00
piyush132000 2ca404dbfd docs: fix code style (#46862)
PR Close #46862
2022-07-18 16:44:21 +00:00
Cédric Exbrayat f9edb776ca refactor(forms): simplify group builder function (#46844)
Applies the same logic that we have in the `control` function.

PR Close #46844
2022-07-15 22:02:20 +00:00
Andrew Kushnir c195c3f73c refactor(core): annotate ComponentMirror object fields as readonly
This commit updates the `inputs` and `outputs` objects of the `ComponentMirror` to mark their properties as readonly.
2022-07-15 14:58:58 -07:00
Andrew Kushnir 114011d2a0 refactor(core): move the code to avoid circular dependencies
This commit refactors the code to move some functions around to avoid circular dependencies in TS imports. The newly added functions are now located in the `packages/core/src/render3/component.ts` file (instead of `packages/core/src/render3/component_ref.ts`), which is a better place for them anyway.
2022-07-15 14:58:58 -07:00
Andrew Kushnir b5153814af feat(core): add reflectComponentType function
This commit introduces a new function that allows creating a object which exposes a number of getters to retrieve information about a given component.
2022-07-15 14:58:58 -07:00
Andrew Kushnir e8e8e5f171 feat(core): add createComponent function
This commit introduces a new function that allows creating a `ComponentRef` instance based on provided Component and a set of options. The function can be used to cover a number of use-cases where the `ComponentFactory` symbol was used previously.
2022-07-15 14:58:58 -07:00
Jessica Janiuk 08fb7bc4ca Revert "Revert "build: update cross-repo angular dependencies to 84dd092 (#46848)" (#46849)" (#46852)
This reverts commit b2a74469de.

PR Close #46852
2022-07-15 21:41:59 +00:00
Jessica Janiuk 6d709273eb Revert "build: update cross-repo angular dependencies to 84dd092 (#46848)" (#46849)
This reverts commit 510f45957a.

PR Close #46849
2022-07-15 17:52:46 +00:00
Angular Robot 571d7debb5 build: update cross-repo angular dependencies to 84dd092 (#46848)
See associated pull request for more information.

PR Close #46848
2022-07-15 16:00:34 +00:00
jnizet b1ba6429d4 docs(core): document that destroyAfterEach is true by default (#46845)
PR Close #46845
2022-07-15 15:50:36 +00:00
Jessica Janiuk 6c104a28e0 build: Add Jessica and Andrew K to docs-infra (#46843)
This adds two people to the docs-infra pullapprove group.

PR Close #46843
2022-07-15 15:46:06 +00:00
Angular Robot 5e09cd1f67 build: update cross-repo angular dependencies (#46834)
See associated pull request for more information.

PR Close #46834
2022-07-14 22:35:09 +00:00
Andrew Scott 6a7b818d94 fix(router): Ensure target RouterStateSnapshot is defined in NavigationError (#46842)
The Router transition observable pipe keeps an outer reference to a `t`
variable for use in the `catchError` operator. However, this variable is
not updated with intermediate state. This commit fixes that so the
`catchError` can access properties that get updated in intermediate
states. Specifically, `RouterStateSnapshot` in the `NavigationError` for
now but could be more in the future.

PR Close #46842
2022-07-14 22:19:27 +00:00
Bob Watson 9ecaa05c7d docs: correct debugging task step (#46832)
Also remove lint errors.

Fix: #42781

PR Close #46832
2022-07-13 22:55:21 +00:00
Andrew Kushnir d36a9e6bf3 refactor(core): improve an error message when ENVIRONMENT_INITIALIZER is not a multi provider (#46829)
Currently if the `ENVIRONMENT_INITIALIZER` token is not configured with `multi: true` flag, the code fails while trying to iterate over the value. This commit checks whether the `ENVIRONMENT_INITIALIZER` token value type is an array and throws a helpful error message.

PR Close #46829
2022-07-13 22:01:15 +00:00
George Kalpakas 4b452ffd3d docs: redirect /guide/ivy to the v12 guide (#46820)
Since Ivy is the default since v13, the Ivy guide (that used to live at
https://angular.io/guide/ivy) has been removed (see #43860). However,
there are certain error messages emitted by the CLI that still point to
it.

This commit address the problem by adding a redirect from
https://angular.io/guide/ivy to https://v12.angular.io/guide/ivy (which
is the last version that includes the guide).

Fixes #46717

PR Close #46820
2022-07-13 21:59:19 +00:00
Kristiyan Kostadinov 3f2d16c823 fix(compiler): inputs/outputs incorrectly parsed in jit mode (#46813)
The `Directive` and `Component` decorators support `inputs` and `outputs` fields which accept an array in the format of `"someInput"` or `"someInput: someAlias"`, however the parsing during JIT compilation was splitting on commas, not on colons, which resulted in incorrect parsing. E.g. `inputs: ["someInput: someAlias"]` was being parsed into `{"someInput: someAlias": "someInput: someAlias"}` instead of `{someInput: "someAlias"}`.

The feature was working by accident, because there's some logic further down in the compiler pipeline that was splitting the strings again.

PR Close #46813
2022-07-13 21:26:51 +00:00
acvi 4dca1842b3 docs: describe smallest typo, "an" -> "a" (#46812)
PR Close #46812
2022-07-13 20:06:30 +00:00
Jessica Janiuk 442b16c551 release: cut the v14.1.0-rc.0 release (#46830) 2022-07-13 12:37:14 -07:00
4162 changed files with 365156 additions and 366497 deletions
+43 -13
View File
@@ -5,35 +5,65 @@
.git
node_modules
dist
aio/content
aio/node_modules
aio/content/example-playground
aio/tools/examples/shared/node_modules
aio/tools/examples/shared/example-scaffold
packages/bazel/node_modules
# All integration test node_modules folders
integration/animations/node_modules
integration/bazel_workspace_tests/bazel_ngtsc_plugin/node_modules
integration/cli-elements-universal/node_modules
integration/cli-hello-world/node_modules
integration/cli-hello-world-ivy-compat/node_modules
integration/cli-hello-world-ivy-i18n/node_modules
integration/cli-hello-world-ivy-minimal/node_modules
integration/cli-hello-world-lazy/node_modules
integration/cli-hello-world-mocha/node_modules
integration/dynamic-compiler/node_modules
integration/forms/node_modules
integration/hello_world__closure/node_modules
integration/hello_world__systemjs_umd/node_modules
integration/i18n/node_modules
integration/injectable-def/node_modules
integration/ivy-i18n/node_modules
integration/ng-add-localize/node_modules
integration/trusted-types/node_modules
integration/forms/node_modules
integration/ng_elements/node_modules
integration/ng_elements_schematics/node_modules
integration/ng_update/node_modules
integration/ng_update_migrations/node_modules
integration/nodenext_resolution/node_modules
integration/npm_package_archives.bzl/node_modules
integration/ngcc/node_modules
integration/platform-server/node_modules
integration/service-worker-schema/node_modules
integration/side-effects/node_modules
integration/standalone-bootstrap/node_modules
integration/terser/node_modules
integration/trusted-types/node_modules
integration/typings_test_rxjs7/node_modules
integration/typings_test_ts49/node_modules
integration/typings_test_ts50/node_modules
integration/typings_test_ts51/node_modules
integration/typings_test_ts36/node_modules
integration/typings_test_ts37/node_modules
# All integration test .yarn_local_cache folders
integration/bazel_workspace_tests/bazel_ngtsc_plugin/.yarn_local_cache
integration/cli-elements-universal/.yarn_local_cache
integration/cli-hello-world/.yarn_local_cache
integration/cli-hello-world-ivy-compat/.yarn_local_cache
integration/cli-hello-world-ivy-i18n/.yarn_local_cache
integration/cli-hello-world-ivy-minimal/.yarn_local_cache
integration/cli-hello-world-lazy/.yarn_local_cache
integration/dynamic-compiler/.yarn_local_cache
integration/hello_world__closure/.yarn_local_cache
integration/hello_world__systemjs_umd/.yarn_local_cache
integration/i18n/.yarn_local_cache
integration/injectable-def/.yarn_local_cache
integration/ivy-i18n/.yarn_local_cache
integration/ng-add-localize/.yarn_local_cache
integration/trusted-types/.yarn_local_cache
integration/forms/.yarn_local_cache
integration/ng_elements/.yarn_local_cache
integration/ng_elements_schematics/.yarn_local_cache
integration/ng_update/.yarn_local_cache
integration/ng_update_migrations/.yarn_local_cache
integration/ngcc/.yarn_local_cache
integration/platform-server/.yarn_local_cache
integration/service-worker-schema/.yarn_local_cache
integration/side-effects/.yarn_local_cache
integration/terser/.yarn_local_cache
integration/typings_test_ts36/.yarn_local_cache
integration/typings_test_ts37/.yarn_local_cache
+14 -51
View File
@@ -1,10 +1,6 @@
# Enable debugging tests with --config=debug
test:debug --test_arg=--node_options=--inspect-brk --test_output=streamed --test_strategy=exclusive --test_timeout=9999 --nocache_test_results
# Do not attempt to de-flake locally.
# On CI we might set this to `3` to run with deflaking.
test --flaky_test_attempts=1
###############################
# Filesystem interactions #
###############################
@@ -56,32 +52,10 @@ build --enable_runfiles
build:release --workspace_status_command="yarn -s ng-dev release build-env-stamp --mode=release"
build:release --stamp
# Building AIO against local Angular deps requires stamping
# versions in Angular packages due to CLI version checks.
build:aio_local_deps --stamp
build:aio_local_deps --workspace_status_command="yarn -s --cwd aio local-workspace-status"
# Snapshots should also be stamped with version control information.
build:snapshot-build --workspace_status_command="yarn -s ng-dev release build-env-stamp --mode=snapshot"
build:snapshot-build --stamp
##########################################################
# AIO architect build configuration #
# See aio/angular.json for available configurations. #
# To build with a partiular configuration: #
# bazel build //aio:build --aio_build_config=<config> #
# Default config is `stable``. #
##########################################################
build --flag_alias=aio_build_config=//aio:flag_aio_build_config
####################################
# AIO first party dep substitution #
# Turn on with #
# --config=aio_local_deps #
####################################
build:aio_local_deps --//aio:flag_aio_local_deps
###############################
# Output #
###############################
@@ -112,16 +86,11 @@ build:remote --define=EXECUTOR=remote
# Set a higher timeout value, just in case.
build:remote --remote_timeout=600
# Bazel detects maximum number of jobs based on host resources.
# Since we run remotely, we can increase this number significantly.
common:remote --jobs=200
# Increase the default number of jobs by 50% because our build has lots of
# parallelism
build:remote --jobs=150
build:remote --google_default_credentials
# Limit the number of test jobs for on an AIO local deps build. The example tests running
# concurrently pushes the circleci executor RAM usage to its limits.
test:aio_local_deps --jobs=24
# Force remote exeuctions to consider the entire run as linux
build:remote --cpu=k8
build:remote --host_cpu=k8
@@ -139,22 +108,6 @@ build:remote --project_id=internal-200822
build:remote --remote_cache=remotebuildexecution.googleapis.com
build:remote --remote_executor=remotebuildexecution.googleapis.com
# Ensure that tags like "no-remote-exec" get propagated to actions created by rules,
# even if the rule implementation does not explicitly pass them to the execution requirements.
# https://bazel.build/reference/command-line-reference#flag--experimental_allow_tags_propagation
common --experimental_allow_tags_propagation
# Disable network access in the sandbox by default. To enable network access
# for a particular target, use:
#
# load("@npm//@angular/build-tooling/bazel/remote-execution:index.bzl", "ENABLE_NETWORK")
# my_target(
# ...,
# exec_properties = ENABLE_NETWORK, # Enables network in remote exec
# tags = ["requires-network"] # Enables network in sandbox
# )
build --nosandbox_default_allow_network
##################################
# Saucelabs tests settings #
# Turn on these settings with #
@@ -172,6 +125,16 @@ test:saucelabs --flaky_test_attempts=1
# --ng_perf will ask the Ivy compiler to produce performance results for each build.
build --flag_alias=ng_perf=//packages/compiler-cli:ng_perf
#####################################################################
# Folders to ignore for integration tests (nested Bazel workspaces) #
#####################################################################
# The nested Bazel integration test workspace should not be analysed as part of
# Skyframe, but should also not be ignored in order to be able to glob for its files.
# TODO: Simplify this once https://github.com/bazelbuild/bazel/issues/12034 is fixed
build --deleted_packages=integration/bazel_workspace_tests/bazel_ngtsc_plugin,integration/bazel_workspace_tests/bazel_ngtsc_plugin/src,integration/bazel_workspace_tests/bazel_ngtsc_plugin/src/hello-world,integration/bazel_workspace_tests/bazel_ngtsc_plugin/test,integration/bazel_workspace_tests/bazel_ngtsc_plugin/test/e2e,integration/bazel_workspace_tests/bazel_ngtsc_plugin/tools,integration/bazel_workspace_tests/bazel_ngtsc_plugin/tools/angular
query --deleted_packages=integration/bazel_workspace_tests/bazel_ngtsc_plugin,integration/bazel_workspace_tests/bazel_ngtsc_plugin/src,integration/bazel_workspace_tests/bazel_ngtsc_plugin/src/hello-world,integration/bazel_workspace_tests/bazel_ngtsc_plugin/test,integration/bazel_workspace_tests/bazel_ngtsc_plugin/test/e2e,integration/bazel_workspace_tests/bazel_ngtsc_plugin/tools,integration/bazel_workspace_tests/bazel_ngtsc_plugin/tools/angular
####################################################
# User bazel configuration
# NOTE: This needs to be the *last* entry in the config.
@@ -179,4 +142,4 @@ build --flag_alias=ng_perf=//packages/compiler-cli:ng_perf
# Load any settings which are specific to the current user. Needs to be *last* statement
# in this config, as the user configuration should be able to overwrite flags from this file.
try-import %workspace%/.bazelrc.user
try-import .bazelrc.user
+4 -5
View File
@@ -1,16 +1,15 @@
# Settings in this file should be OS agnostic. Use the bazel.<OS>.rc files for OS specific settings.
# Don't be spammy in the logs
build --noshow_progress
# Print all the options that apply to the build.
# This helps us diagnose which options override others
# (e.g. /etc/bazel.bazelrc vs. tools/bazel.rc)
build --announce_rc
# Retry in the event of flakes, eg. https://circleci.com/gh/angular/angular/31309
# By default this is set in the project `.bazelrc` to `1`.
test --flaky_test_attempts=3
test --flaky_test_attempts=2
# More details on failures
build --verbose_failures=true
# CI supports colors but Bazel does not detect it.
common --color=yes
+2 -2
View File
@@ -12,10 +12,10 @@ build --repository_cache=/home/circleci/bazel_repository_cache
# Workaround https://github.com/bazelbuild/bazel/issues/3645
# Bazel doesn't calculate the memory ceiling correctly when running under Docker.
# Limit Bazel to consuming resources that fit in CircleCI "2xlarge+" class
# Limit Bazel to consuming resources that fit in CircleCI "xlarge" class
# https://circleci.com/docs/2.0/configuration-reference/#resource_class
build --local_cpu_resources=20
build --local_ram_resources=40960
build --local_ram_resources=32768
# All build executed remotely should be done using our RBE configuration.
build:remote --google_default_credentials
+215 -91
View File
@@ -23,47 +23,50 @@ version: 2.1
# (See https://support.circleci.com/hc/en-us/articles/360012618473-Creating-a-daily-cache.)
# **NOTE 2 **: If you change the cache key prefix, also sync the cache_key_fallback to match.
# **NOTE 3 **: Keep the static part of the cache key as prefix to enable correct fallbacks.
# **NOTE 4 **: To make sure modified patches can be applied without needing to manually update the cache key,
# the fallback cache key will not match if patches have been modified.
# See https://circleci.com/docs/2.0/caching/#restoring-cache for how prefixes work in CircleCI.
var_3: &cache_key v8-angular-node-16-{{ checksum "month.txt" }}-{{ checksum "patches.hash"}}-{{ checksum ".bazelversion" }}-{{ checksum "yarn.lock" }}-{{ checksum "WORKSPACE" }}-{{ checksum "aio/yarn.lock" }}
var_3: &cache_key v7-angular-node-14-{{ checksum "month.txt" }}-{{ checksum ".bazelversion" }}-{{ checksum "yarn.lock" }}-{{ checksum "WORKSPACE" }}-{{ checksum "aio/yarn.lock" }}
# We invalidate the cache if the Bazel version changes because otherwise the `bazelisk` cache
# folder will contain all previously used versions and ultimately cause the cache restoring to
# be slower due to its growing size.
var_4: &cache_key_fallback v8-angular-node-16-{{ checksum "month.txt" }}-{{ checksum "patches.hash"}}-{{ checksum ".bazelversion" }}
var_4: &cache_key_fallback v7-angular-node-14-{{ checksum "month.txt" }}-{{ checksum ".bazelversion" }}
# Windows needs its own cache key because binaries in node_modules are different.
var_3_win: &cache_key_win v10-angular-win-node-16-{{ checksum "month.txt" }}-{{ checksum "patches.hash"}}-{{ checksum ".bazelversion" }}-{{ checksum "yarn.lock" }}-{{ checksum "WORKSPACE" }}
var_4_win: &cache_key_win_fallback v10-angular-win-node-16-{{ checksum "month.txt" }}-{{ checksum "patches.hash"}}-{{ checksum ".bazelversion" }}
var_3_win: &cache_key_win v9-angular-win-node-14-{{ checksum "month.txt" }}-{{ checksum ".bazelversion" }}-{{ checksum "yarn.lock" }}-{{ checksum "WORKSPACE" }}
var_4_win: &cache_key_win_fallback v9-angular-win-node-14-{{ checksum "month.txt" }}-{{ checksum ".bazelversion" }}
# Cache key for the `components-repo-unit-tests` job. **Note** when updating the SHA in the
# cache keys also update the SHA for the "COMPONENTS_REPO_COMMIT" environment variable.
var_5: &components_repo_unit_tests_cache_key v1-angular-components-{{ checksum "month.txt" }}-7a24e95bafbdeb697f74a48e275c2442bcbefc74
var_6: &components_repo_unit_tests_cache_key_fallback v1-angular-components-{{ checksum "month.txt" }}
# Workspace initially persisted by the `setup` job, and then enhanced by `build-npm-packages`.
# https://circleci.com/docs/2.0/workflows/#using-workspaces-to-share-data-among-jobs
# https://circleci.com/blog/deep-diving-into-circleci-workspaces/
var_5: &workspace_location ~/
var_7: &workspace_location ~/
# Filter to run a job on builds for pull requests only.
var_6: &only_on_pull_requests
var_8: &only_on_pull_requests
filters:
branches:
only:
- /pull\/\d+/
# Filter to skip a job on builds for pull requests.
var_7: &skip_on_pull_requests
var_9: &skip_on_pull_requests
filters:
branches:
ignore:
- /pull\/\d+/
# Filter to run a job on builds for the main branch only.
var_8: &only_on_main_branch
var_10: &only_on_main_branch
filters:
branches:
only:
- main
# Filter to run a job on all releasable branches.
var_9: &only_release_branches
var_11: &only_release_branches
filters:
branches:
only:
@@ -74,7 +77,6 @@ var_9: &only_release_branches
# https://circleci.com/developer/orbs.
orbs:
node: circleci/node@5.0.1
devinfra: angular/dev-infra@1.0.8
# Executor Definitions
# https://circleci.com/docs/2.0/reusing-config/#authoring-reusable-executors
@@ -89,7 +91,7 @@ executors:
type: string
default: medium
docker:
- image: cimg/node:16.14.0
- image: cimg/node:16.10.0
resource_class: << parameters.resource_class >>
working_directory: ~/ng
@@ -99,7 +101,7 @@ executors:
type: string
default: medium
docker:
- image: cimg/node:16.14.0-browsers
- image: cimg/node:16.10.0-browsers
resource_class: << parameters.resource_class >>
working_directory: ~/ng
@@ -142,7 +144,6 @@ commands:
CIRCLE_GIT_BASE_REVISION: << pipeline.git.base_revision >>
CIRCLE_GIT_REVISION: << pipeline.git.revision >>
command: ./.circleci/env.sh
- devinfra/setup-bazel-remote-exec
- run:
# Configure git as the CircleCI `checkout` command does.
# This is needed because we only checkout on the setup job.
@@ -150,7 +151,7 @@ commands:
name: Configure git
command: |
mkdir -p ~/.ssh
echo 'github.com ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCj7ndNxQowgcQnjshcLrqPEiiphnt+VTTvDP6mHBL9j1aNUkY4Ue1gvwnGLVlOhGeYrnZaMgRK6+PKCUXaDbC7qtbW8gIkhL7aGCsOr/C56SJMy/BCZfxd1nWzAOxSDPgVsmerOBYfNqltV9/hWCqBywINIR+5dIg6JTJ72pcEpEjcYgXkE2YEFXV1JHnsKgbLWNlhScqb2UmyRkQyytRLtL+38TGxkxCflmO+5Z8CSSNY7GidjMIZ7Q4zMjA2n1nGrlTDkzwDCsw+wqFPGQA179cnfGWOWRVruj16z6XyvxvjJwbz0wQZ75XK5tKSb7FNyeIEs4TT4jk+S4dhPeAUC5y+bDYirYgM4GC7uEnztnZyaVWQ7B381AK4Qdrwt51ZqExKbQpTUNn+EjqoTwvqNj4kqx5QUCI0ThS/YkOxJCXmPUWZbhjpCg56i+2aB6CmK2JGhn57K5mj0MNdBXA4/WnwH6XoPWJzK5Nyu2zB3nAZp+S5hpQs+p1vN1/wsjk=' >> ~/.ssh/known_hosts
echo 'github.com ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAq2A7hRGmdnm9tUDbO9IDSwBK6TbQa+PXYPCPy6rbTrTtw7PHkccKrpp0yVhp5HdEIcKr6pLlVDBfOLX9QUsyCOV0wzfjIJNlGEYsdlLJizHhbn2mUjvSAHQqZETYP81eFzLQNnPHt4EVVUh7VfDESU84KezmD5QlWpXLmvU31/yMf+Se8xhHTvKSCZIFImWwoG6mbUoWf9nzpIoaSjB+weqqUUmpaaasXVal72J+UX2B+2RPW3RcT0eOzQgqlJL3RKrTJvdsjE3JEAvGq3lGHSZXy28G3skua2SmVi/w4yCE6gbODqnTWlg7+wC604ydGXA8VJiS5ap43JXiUFFAaQ==' >> ~/.ssh/known_hosts
git config --global url."ssh://git@github.com".insteadOf "https://github.com" || true
git config --global gc.auto 0 || true
@@ -184,12 +185,6 @@ commands:
# snapshot stamping would have the `-with-local-changes` suffix.
command: date +%Y-%m > month.txt
capture_patches_for_cache_key:
description: Hashes all patches so that the cache can be reset upon changes.
steps:
- run: |
md5sum tools/esm-interop/patches/npm/* > patches.hash
yarn_install:
description: Install dependencies
steps:
@@ -222,11 +217,20 @@ jobs:
steps:
- checkout
- save_month_to_file
- capture_patches_for_cache_key
- init_environment
- devinfra/rebase-pr-on-target-branch:
base_revision: << pipeline.git.base_revision >>
head_revision: << pipeline.git.revision >>
- run:
name: Rebase PR on target branch
# After checkout, rebase on top of target branch.
command: >
if [[ -n "${CIRCLE_PR_NUMBER}" ]]; then
# User is required for rebase.
git config user.name "angular-ci"
git config user.email "angular-ci"
# Rebase PR on top of target branch.
node .circleci/rebase-pr.js
else
echo "This build is not over a PR, nothing to do."
fi
# This cache is saved in the build-npm-packages so that Bazel cache is also included.
- restore_cache:
keys:
@@ -249,6 +253,20 @@ jobs:
- ./ng
- ./bazel_repository_cache
lint:
executor: default-executor
steps:
- custom_attach_workspace
- init_environment
- run: yarn -s tslint
- run: yarn -s ng-dev format changed $CI_GIT_BASE_REVISION --check
- run: yarn -s ts-circular-deps:check
- run: yarn -s ng-dev pullapprove verify
- run: yarn -s ng-dev ngbot verify
- run: yarn -s ng-dev commit-message validate-range $CI_GIT_BASE_REVISION $CI_GIT_REVISION
- run: yarn -s check-tooling-setup
test:
executor:
name: test-browser-executor
@@ -260,6 +278,7 @@ jobs:
steps:
- custom_attach_workspace
- init_environment
- install_java
- run:
# We need to explicitly specify the --symlink_prefix option because otherwise we would
# not be able to easily find the output bin directory when uploading artifacts for size
@@ -284,8 +303,6 @@ jobs:
- store_artifacts:
path: dist/bin/packages/core/test/bundling/todo/bundle.min.js.br
destination: core/todo/bundle.br
- notify_webhook_on_fail:
webhook_url_env_var: SLACK_REACTIVITY_CHANNEL_WEBHOOK_URL
# NOTE: This is currently limited to main builds only. See the `monitoring` configuration.
saucelabs:
@@ -295,45 +312,46 @@ jobs:
# container for this job. This is necessary because we launch a lot of browsers concurrently
# and therefore the tunnel and Karma need to process a lot of file requests and tests.
resource_class: xlarge
environment:
NUMBER_OF_PARALLEL_BROWSERS: 2
steps:
- custom_attach_workspace
- init_environment
- init_saucelabs_environment
- run:
name: Start Saucelabs daemon service
command: yarn bazel run //tools/saucelabs-daemon/background-service -- ${NUMBER_OF_PARALLEL_BROWSERS}
background: true
- run:
name: Run Bazel tests on Saucelabs
# See /tools/saucelabs/README.md for more info
command: |
yarn bazel run //tools/saucelabs:sauce_service_setup
TESTS=$(./node_modules/.bin/bazelisk query --output label '(kind(karma_web_test, ...) intersect attr("tags", "saucelabs", ...)) except attr("tags", "fixme-saucelabs", ...)')
yarn bazel test --config=saucelabs --jobs=${NUMBER_OF_PARALLEL_BROWSERS} ${TESTS}
yarn bazel test --config=saucelabs ${TESTS}
yarn bazel run //tools/saucelabs:sauce_service_stop
no_output_timeout: 40m
- notify_webhook_on_fail:
webhook_url_env_var: SLACK_DEV_INFRA_CI_FAILURES_WEBHOOK_URL
test_aio:
executor:
name: test-browser-executor
resource_class: 2xlarge+
executor: test-browser-executor
steps:
- custom_attach_workspace
- init_environment
# Run all aio tests
- run:
command: yarn --cwd aio test:ci
no_output_timeout: 20m
# Build aio
- run: yarn --cwd aio build --progress=false
# Lint the code
- run: yarn --cwd aio lint
# Run unit tests
- run: yarn --cwd aio test --progress=false --watch=false
# Run e2e tests
- run: yarn --cwd aio e2e --configuration=ci
# Run PWA-score tests
- run: yarn --cwd aio test-pwa-score-localhost $CI_AIO_MIN_PWA_SCORE
# Run accessibility tests
- run: yarn --cwd aio test-a11y-score-localhost
# Check the bundle sizes.
- run: yarn --cwd aio payload-size
# Run unit tests for Firebase redirects
- run: yarn --cwd aio redirects-test
deploy_aio:
executor:
name: test-browser-executor
resource_class: 2xlarge+
executor: test-browser-executor
steps:
- custom_attach_workspace
- init_environment
@@ -342,20 +360,75 @@ jobs:
- run: yarn --cwd aio deploy-production
test_aio_local:
executor:
name: test-browser-executor
resource_class: 2xlarge+
executor: test-browser-executor
steps:
- custom_attach_workspace
- init_environment
# Run all aio tests (with local Angular packages)
- run:
command: yarn --cwd aio test-local:ci
no_output_timeout: 20m
# Build aio (with local Angular packages)
- run: yarn --cwd aio build-local-ci
# Run unit tests
- run: yarn --cwd aio test --progress=false --watch=false
# Run e2e tests
- run: yarn --cwd aio e2e --configuration=ci
# Run PWA-score tests
- run: yarn --cwd aio test-pwa-score-localhost $CI_AIO_MIN_PWA_SCORE
# Check the bundle sizes.
- run: yarn --cwd aio payload-size aio-local
test_aio_tools:
executor: default-executor
steps:
- custom_attach_workspace
- init_environment
# Install
- run: yarn --cwd aio install --frozen-lockfile --non-interactive --cache-folder ~/.cache/yarn
- run: yarn --cwd aio extract-cli-command-docs
# Run tools tests
- run: yarn --cwd aio tools-test
- run: ./aio/aio-builds-setup/scripts/test.sh
test_docs_examples:
executor:
name: test-browser-executor
resource_class: xlarge
parallelism: 5
steps:
- custom_attach_workspace
- init_environment
# Install aio
- run: yarn --cwd aio install --frozen-lockfile --non-interactive --cache-folder ~/.cache/yarn
# Run examples tests. The "CIRCLE_NODE_INDEX" will be set if "parallelism" is enabled.
# Since the parallelism is set to "5", there will be five parallel CircleCI containers.
# with either "0", "1", etc as node index. This can be passed to the "--shard" argument.
- run: yarn --cwd aio example-e2e --setup --local --cliSpecsConcurrency=5 --shard=${CIRCLE_NODE_INDEX}/${CIRCLE_NODE_TOTAL}
# This job should only be run on PR builds, where `CI_PULL_REQUEST` is not `false`.
aio_preview:
executor: default-executor
environment:
AIO_SNAPSHOT_ARTIFACT_PATH: &aio_preview_artifact_path 'aio/tmp/snapshot.tgz'
steps:
- custom_attach_workspace
- init_environment
- run: ./aio/scripts/build-artifacts.sh $AIO_SNAPSHOT_ARTIFACT_PATH $CI_PULL_REQUEST $CI_COMMIT
- store_artifacts:
path: *aio_preview_artifact_path
# The `destination` needs to be kept in sync with the value of
# `AIO_ARTIFACT_PATH` in `aio/aio-builds-setup/Dockerfile`
destination: aio/dist/aio-snapshot.tgz
- run: node ./aio/scripts/create-preview $CIRCLE_BUILD_NUM
# This job should only be run on PR builds, where `CI_PULL_REQUEST` is not `false`.
test_aio_preview:
executor: test-browser-executor
steps:
- custom_attach_workspace
- init_environment
- run: yarn --cwd aio install --frozen-lockfile --non-interactive --cache-folder ~/.cache/yarn
- run:
name: Wait for preview and run tests
command: node aio/scripts/test-preview.js $CI_PULL_REQUEST $CI_COMMIT $CI_AIO_MIN_PWA_SCORE
# The `build-npm-packages` tasks exist for backwards-compatibility with old scripts and
# tests that rely on the pre-Bazel `dist/packages-dist` output structure (build.sh).
# Having multiple jobs that independently build in this manner duplicates some work; we build
@@ -440,8 +513,11 @@ jobs:
# Clear the global setting
- run: git config --global --unset "url.ssh://git@github.com.insteadof"
- run:
name: Prepare GitHub credentials
command: echo "https://${SNAPSHOT_BUILDS_GITHUB_TOKEN}:@github.com" > ${HOME}/.git_credentials
name: Decrypt github credentials
# We need ensure that the same default digest is used for encoding and decoding with
# OpenSSL. OpenSSL versions might have different default digests which can cause
# decryption failures based on the installed OpenSSL version. https://stackoverflow.com/a/39641378/4317734
command: 'openssl aes-256-cbc -d -in .circleci/github_token -md md5 -k "${KEY}" -out ~/.git_credentials'
- run: ./scripts/ci/publish-build-artifacts.sh
aio_misc:
@@ -525,6 +601,8 @@ jobs:
mkdir -p packages/common/locales/extra
cp dist/bin/packages/common/locales/*.ts packages/common/locales
cp dist/bin/packages/common/locales/extra/*.ts packages/common/locales/extra
# Build ZoneJS so that it can be loaded globally in the Karma tests.
- run: yarn bazel build //packages/zone.js:npm_package
# Build the bundle for all tests to run within Saucelabs
- run: node tools/legacy-saucelabs/build-saucelabs-test-bundle.mjs
- run:
@@ -539,6 +617,59 @@ jobs:
name: Stop Saucelabs tunnel service
command: ./tools/saucelabs/sauce-service.sh stop
# Job that runs all unit tests of the `angular/components` repository.
components-repo-unit-tests:
executor:
name: default-executor
resource_class: xlarge
steps:
- custom_attach_workspace
- init_environment
# Restore the cache before cloning the repository because the clone script re-uses
# the restored repository if present. This reduces the amount of times the components
# repository needs to be cloned (this is slow and increases based on commits in the repo).
- restore_cache:
keys:
- *components_repo_unit_tests_cache_key
# Whenever the `angular/components` SHA is updated, the cache key will no longer
# match. The fallback cache will still match, and CircleCI will restore the most
# recently cached repository folder. Without the fallback cache, we'd need to download
# the repository from scratch and it would slow down the job. This is because we can't
# clone the repository with reduced `--depth`, but rather need to clone the whole
# repository to be able to support arbitrary SHAs.
- *components_repo_unit_tests_cache_key_fallback
- run:
name: 'Fetching angular/components repository'
command: ./scripts/ci/clone_angular_components_repo.sh
- run:
# Run yarn install to fetch the Bazel binaries as used in the components repo.
name: Installing dependencies.
command: yarn --cwd ${COMPONENTS_REPO_TMP_DIR} install --frozen-lockfile --non-interactive --cache-folder ~/.cache/yarn
- save_cache:
key: *components_repo_unit_tests_cache_key
paths:
# Temporary directory must be kept in sync with the `$COMPONENTS_REPO_TMP_DIR` env
# variable. It needs to be hardcoded here, because env variables interpolation is
# not supported.
- '/tmp/angular-components-repo'
- run:
# Copy zone.js package to dist/packages-dist, so `angular/components` can also use the local zone.js build.
name: Setting up zone.js release packages.
command: cp -r dist/zone.js-dist/zone.js dist/packages-dist/
- run:
# Updates the `angular/components` `package.json` file to refer to the release output
# inside the `packages-dist` directory.
name: Setting up framework release packages.
command: node scripts/ci/update-framework-deps-to-dist-packages.js ${COMPONENTS_REPO_TMP_DIR}/package.json dist/packages-dist/
- run:
# Run `yarn install` again to install the Angular packages from `packages-dist/` and update the lockfile.
# NOTE: We cannot rely on Bazel to run `yarn install`, because it uses the `--frozen-lockfile` flag and fails.
name: Installing local Angular packages.
command: yarn --cwd ${COMPONENTS_REPO_TMP_DIR} install --non-interactive --cache-folder ~/.cache/yarn
- run:
name: 'Running `angular/components` unit tests'
command: ./scripts/ci/run_angular_components_unit_tests.sh
test_zonejs:
executor:
name: default-executor
@@ -547,30 +678,15 @@ jobs:
- custom_attach_workspace
- init_environment
- install_java
- run: |
yarn bazel build -- \
//packages/zone.js/bundles:zone.umd.js \
//packages/zone.js:npm_package \
//packages/zone.js/test/closure:closure_js \
//packages/zone.js:zone_externs
- run: |
mkdir -p packages/zone.js/build/
mkdir -p packages/zone.js/build/test/
mkdir -p packages/zone.js/test/
cp dist/bin/packages/zone.js/bundles/zone.umd.js packages/zone.js/build/zone.umd.js
cp dist/bin/packages/zone.js/npm_package/bundles/zone-mix.umd.js ./packages/zone.js/test/extra/
cp dist/bin/packages/zone.js/npm_package/bundles/zone-patch-electron.umd.js ./packages/zone.js/test/extra/
cp dist/bin/packages/zone.js/test/closure/zone.closure.mjs ./packages/zone.js/build/test/zone.closure.mjs
cp dist/bin/packages/zone.js/zone_externs.js ./packages/zone.js/build/zone_externs.js
# Install
- run: yarn --cwd packages/zone.js install --frozen-lockfile --non-interactive --cache-folder ~/.cache/yarn
# Run zone.js tools tests
- run: yarn --cwd packages/zone.js promisetest
- run: yarn --cwd packages/zone.js promisefinallytest
- run: yarn bazel build //packages/zone.js:npm_package &&
cp dist/bin/packages/zone.js/npm_package/bundles/zone-mix.umd.js ./packages/zone.js/test/extra/ &&
cp dist/bin/packages/zone.js/npm_package/bundles/zone-patch-electron.umd.js ./packages/zone.js/test/extra/ &&
cp dist/bin/packages/zone.js/npm_package/bundles/zone.umd.js ./packages/zone.js/build/test/closure/zone.js
- run: yarn --cwd packages/zone.js jest:test
- run: yarn --cwd packages/zone.js jest:nodetest
- run: yarn --cwd packages/zone.js electrontest
@@ -584,29 +700,16 @@ jobs:
executor: windows-executor
steps:
- checkout
- save_month_to_file
- init_environment
# Setup the proper NodeJS version. The image comes pre-installed with `nvm-windows`.
# Note: This happens immediately after checkout because environment initialization logic
# might rely on Node and shouldn't use the older image-provided NodeJS version.
- run:
name: Install NodeJS version from `.nvmrc`.
command: |
nvm on
nvm install $(cat .nvmrc)
nvm use $(cat .nvmrc)
- run:
name: Debug information for flakiness of Windows job
command: |
node -v
yarn -v
which node
which yarn
echo "Path: $PATH"
yarn node -v
- save_month_to_file
- capture_patches_for_cache_key
- init_environment
- restore_cache:
keys:
- *cache_key_win
@@ -655,6 +758,9 @@ workflows:
filters:
branches:
ignore: g3
- lint:
requires:
- setup
- test:
requires:
- setup
@@ -671,8 +777,22 @@ workflows:
requires:
- test_aio
- test_aio_local:
requires:
- build-npm-packages
- test_aio_tools:
requires:
- build-npm-packages
- test_docs_examples:
requires:
- build-npm-packages
- aio_preview:
# Only run on PR builds. (There can be no previews for non-PR builds.)
<<: *only_on_pull_requests
requires:
- setup
- test_aio_preview:
requires:
- aio_preview
- publish_packages_as_artifacts:
requires:
- build-npm-packages
@@ -683,10 +803,14 @@ workflows:
- test
# Only publish if `aio`/`docs` tests using the locally built Angular packages pass
- test_aio_local
- test_docs_examples
# Get the artifacts to publish from the build-packages-dist job
# since the publishing script expects the legacy outputs layout.
- build-npm-packages
- legacy-unit-tests-saucelabs
- components-repo-unit-tests:
requires:
- build-npm-packages
- test_zonejs:
requires:
- setup
+8 -1
View File
@@ -1,4 +1,11 @@
####################################################################################################
# Decrypt GCP Credentials and store them as the Google default credentials.
####################################################################################################
mkdir -p "$HOME/.config/gcloud";
openssl aes-256-cbc -d -in "${PROJECT_ROOT}/.circleci/gcp_token" \
-md md5 -k "$CIRCLE_PROJECT_REPONAME" -out "$HOME/.config/gcloud/application_default_credentials.json"
####################################################################################################
# Set bazel configuration for CircleCI runs.
####################################################################################################
cp "${PROJECT_ROOT}/.circleci/bazel.linux.rc" ".bazelrc.user";
cp "${PROJECT_ROOT}/.circleci/bazel.linux.rc" "$HOME/.bazelrc";
+14
View File
@@ -61,6 +61,20 @@ setPublicVar SAUCE_TUNNEL_IDENTIFIER "angular-framework-${CIRCLE_BUILD_NUM}-${CI
# acquire CircleCI instances for too long if sauceconnect failed, we need a connect timeout.
setPublicVar SAUCE_READY_FILE_TIMEOUT 120
####################################################################################################
# Define environment variables for the `angular/components` repo unit tests job.
####################################################################################################
# We specifically use a directory within "/tmp" here because we want the cloned repo to be
# completely isolated from angular/angular in order to avoid any bad interactions between
# their separate build setups. **NOTE**: When updating the temporary directory, also update
# the `save_cache` path configuration in `config.yml`
setPublicVar COMPONENTS_REPO_TMP_DIR "/tmp/angular-components-repo"
setPublicVar COMPONENTS_REPO_URL "https://github.com/angular/components.git"
setPublicVar COMPONENTS_REPO_BRANCH "main"
# **NOTE**: When updating the commit SHA, also update the cache key in the CircleCI `config.yml`.
setPublicVar COMPONENTS_REPO_COMMIT "7a24e95bafbdeb697f74a48e275c2442bcbefc74"
####################################################################################################
# Create shell script in /tmp for Bazel actions to access CI envs without
# busting the cache. Used by payload-size.sh script in integration tests.
+9 -5
View File
@@ -1,13 +1,17 @@
####################################################################################################
# Decrypt GCP Credentials and store them as the Google default credentials.
####################################################################################################
mkdir -p "${APPDATA}/gcloud/";
openssl aes-256-cbc -d -in "${PROJECT_ROOT}/.circleci/gcp_token" \
-md md5 -k "$CIRCLE_PROJECT_REPONAME" -out "${APPDATA}/gcloud/application_default_credentials.json"
####################################################################################################
# Set bazel configuration for CircleCI runs.
####################################################################################################
cp "${PROJECT_ROOT}/.circleci/bazel.windows.rc" ".bazelrc.user";
cp "${PROJECT_ROOT}/.circleci/bazel.windows.rc" "${USERPROFILE}/.bazelrc";
# Override the `PATH` environment variable so that the windows-nvm NodeJS version
# always has precedence over potential existing NodeJS versions from the image.
setPublicVar PATH "/c/Program Files/nodejs/:$PATH"
# Expose the Bazelisk version. We need to run Bazelisk globally since Windows has problems launching
# Bazel from a node modules directoy that might be modified by the Bazel Yarn install then.
setPublicVar BAZELISK_VERSION \
"$(cd ${PROJECT_ROOT}; node -p 'require("./package.json").devDependencies["@bazel/bazelisk"]')"
"$(cd ${PROJECT_ROOT}; node -p 'require("./package.json").devDependencies["@bazel/bazelisk"]')"
Binary file not shown.
Binary file not shown.
+208
View File
@@ -0,0 +1,208 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
/**
* Rebases the current branch on top of the GitHub PR target branch.
*
* **Context:**
* Since a GitHub PR is not necessarily up to date with its target branch, it is useful to rebase
* prior to testing it on CI to ensure more up to date test results.
*
* **NOTE:**
* This script cannot use external dependencies or be compiled because it needs to run before the
* environment is setup.
* Use only features supported by the NodeJS versions used in the environment.
*/
// tslint:disable:no-console
const {execSync} = require('child_process');
/** A regex to select a ref that matches our semver refs. */
const semverRegex = /^(\d+)\.(\d+)\.x$/;
// Run
_main().catch(err => {
console.log('Failed to rebase on top of target branch.\n');
console.error(err);
process.exitCode = 1;
});
// Helpers
async function _main() {
const refs = await getRefsAndShasForChange();
// Log known refs and shas
console.log(`--------------------------------`);
console.log(` Target Branch: ${refs.base.ref}`);
console.log(` Latest Commit for Target Branch: ${refs.target.latestSha}`);
console.log(` Latest Commit for PR: ${refs.base.latestSha}`);
console.log(` First Common Ancestor SHA: ${refs.commonAncestorSha}`);
console.log(`--------------------------------`);
console.log();
// Get the count of commits between the latest commit from origin and the common ancestor SHA.
const commitCount =
exec(`git rev-list --count origin/${refs.base.ref}...${refs.commonAncestorSha}`);
console.log(`Checking ${commitCount} commits for changes in the CircleCI config file.`);
// Check if the files changed between the latest commit from origin and the common ancestor SHA
// includes the CircleCI config.
const circleCIConfigChanged = exec(`git diff --name-only origin/${refs.base.ref} ${
refs.commonAncestorSha} -- .circleci/config.yml`);
if (!!circleCIConfigChanged) {
throw Error(`
CircleCI config on ${refs.base.ref} has been modified since commit
${refs.commonAncestorSha.slice(0, 7)}, which this PR is based on.
Please rebase the PR on ${refs.base.ref} after fetching from upstream.
Rebase instructions for PR Author, please run the following commands:
git fetch upstream ${refs.base.ref};
git checkout ${refs.target.ref};
git rebase upstream/${refs.base.ref};
git push --force-with-lease;
`);
} else {
console.log('No change found in the CircleCI config file, continuing.');
}
console.log();
// Rebase the PR.
exec(`git rebase origin/${refs.base.ref}`);
console.log(`Rebased current branch onto ${refs.base.ref}.`);
}
/**
* Sort a list of fullpath refs into a list and then provide the first entry.
*
* The sort order will first find the the main branch, and then any semver ref, followed
* by the rest of the refs in the order provided.
*
* Branches are sorted in this order as work is primarily done on main branches, and
* otherwise on a semver branch. If neither of those were to match, the most
* likely correct branch will be the first one encountered in the list.
*/
function getRefFromBranchList(gitOutput) {
const branches = gitOutput.split('\n').map(b => b.split('/').slice(1).join('/').trim());
return branches.sort((a, b) => {
if (a === 'main') {
return -1;
}
if (b === 'main') {
return 1;
}
const aIsSemver = semverRegex.test(a);
const bIsSemver = semverRegex.test(b);
if (aIsSemver && bIsSemver) {
const [, aMajor, aMinor] = a.match(semverRegex);
const [, bMajor, bMinor] = b.match(semverRegex);
return parseInt(bMajor, 10) - parseInt(aMajor, 10) ||
parseInt(aMinor, 10) - parseInt(bMinor, 10) || 0;
}
if (aIsSemver) {
return -1;
}
if (bIsSemver) {
return 1;
}
return 0;
})[0];
}
/**
* Get the full sha of the ref provided.
*
* example: 1bc0c1a6c01ede7168f22fa9b3508ba51f1f464e
*/
function getShaFromRef(ref) {
return exec(`git rev-parse ${ref}`);
}
/**
* Get the list of branches which contain the provided sha, sorted in descending order
* by committerdate.
*
* example:
* upstream/main
* upstream/9.0.x
* upstream/test
* upstream/1.1.x
*/
function getBranchListForSha(sha, remote) {
return exec(`git branch -r '${remote}/*' --sort=-committerdate --contains ${sha}`);
}
/** Get the common ancestor sha of the two provided shas. */
function getCommonAncestorSha(sha1, sha2) {
return exec(`git merge-base ${sha1} ${sha2}`);
}
/**
* Adds the remote to git, if it doesn't already exist. Returns a boolean indicating
* whether the remote was added by the command.
*/
function addAndFetchRemote(owner, name) {
const remoteName = `${owner}_${name}`;
exec(`git remote add ${remoteName} https://github.com/${owner}/${name}.git`, true);
exec(`git fetch ${remoteName}`);
return remoteName;
}
/** Get the ref and latest shas for the provided sha on a specific remote. */
function getRefAndShas(sha, owner, name) {
const remoteName = addAndFetchRemote(owner, name);
// Get the ref on the remote for the sha provided.
const branches = getBranchListForSha(sha, remoteName);
const ref = getRefFromBranchList(branches);
// Get the latest sha on the discovered remote ref.
const latestSha = getShaFromRef(`${remoteName}/${ref}`);
return {remote: remoteName, ref, latestSha, sha};
}
/** Gets the refs and shas for the base and target of the current environment. */
function getRefsAndShasForChange() {
const base = getRefAndShas(
process.env['CI_GIT_BASE_REVISION'], process.env['CI_REPO_OWNER'],
process.env['CI_REPO_NAME']);
const target = getRefAndShas(
process.env['CI_GIT_REVISION'], process.env['CI_PR_USERNAME'], process.env['CI_PR_REPONAME']);
const commonAncestorSha = getCommonAncestorSha(base.sha, target.sha);
return {
base,
target,
commonAncestorSha,
};
}
/**
* Synchronously executes the command.
*
* Return the trimmed stdout as a string, with an added attribute of the exit code.
*/
function exec(command, ignoreError = false) {
try {
return execSync(command, {stdio: 'pipe'}).toString().trim();
} catch (err) {
if (ignoreError) {
return '';
}
throw err;
}
}
+105
View File
@@ -0,0 +1,105 @@
#!/usr/bin/env node
/**
* Usage (cli):
* ```
* node create-preview <build-number> <job-name> <webhook-url>
* ```
*
* Usage (JS):
* ```js
* require('./trigger-webhook').
* triggerWebhook(buildNumber, jobName, webhookUrl).
* then(...);
* ```
*
* Triggers a notification webhook with CircleCI specific info.
*
* It can be used for notifying external servers and trigger operations based on CircleCI job status
* (e.g. triggering the creation of a preview based on previously stored build atrifacts).
*
* The body of the sent payload is of the form:
* ```json
* {
* "payload": {
* "build_num": ${buildNumber}
* "build_parameters": {
* "CIRCLE_JOB": "${jobName}"
* }
* }
* }
* ```
*
* When used from JS, it returns a promise which resolves to an object of the form:
* ```json
* {
* "statucCode": ${statusCode},
* "responseText": "${responseText}"
* }
* ```
*
* NOTE:
* - When used from the cli, the command will exit with an error code if the response's status code
* is outside the [200, 400) range.
* - When used from JS, the returned promise will be resolved, even if the response's status code is
* outside the [200, 400) range. It is up to the caller to decide how this should be handled.
*/
// Imports
const {request} = require('https');
// Exports
module.exports = {
triggerWebhook,
};
// Run
if (require.resolve === module) {
_main(process.argv.slice(2));
}
// Helpers
function _main(args) {
triggerWebhook(...args)
.then(
({statusCode, responseText}) => (200 <= statusCode && statusCode < 400) ?
console.log(`Status: ${statusCode}\n${responseText}`) :
Promise.reject(new Error(`Request failed (status: ${statusCode}): ${responseText}`)))
.catch(err => {
console.error(err);
process.exit(1);
});
}
function postJson(url, data) {
return new Promise((resolve, reject) => {
const opts = {method: 'post', headers: {'Content-Type': 'application/json'}};
const onResponse = res => {
const statusCode = res.statusCode || -1;
let responseText = '';
res.on('error', reject)
.on('data', d => responseText += d)
.on('end', () => resolve({statusCode, responseText}));
};
request(url, opts, onResponse).on('error', reject).end(JSON.stringify(data));
});
}
async function triggerWebhook(buildNumber, jobName, webhookUrl) {
if (!buildNumber || !jobName || !webhookUrl || isNaN(buildNumber)) {
throw new Error(
'Missing or invalid arguments.\n' +
'Expected: buildNumber (number), jobName (string), webhookUrl (string)');
}
const data = {
payload: {
build_num: +buildNumber,
build_parameters: {CIRCLE_JOB: jobName},
},
};
return postJson(webhookUrl, data);
}
+1 -1
View File
@@ -1,7 +1,7 @@
# Image metadata and config.
# Ideally, the Node.js version should match what we use on CI.
# See `executors > default-executor` in `.circleci/config.yml`.
FROM cimg/node:16.13-browsers
FROM cimg/node:14-browsers
LABEL name="Angular dev environment" \
-1
View File
@@ -23,7 +23,6 @@ body:
- router
- service-worker
- upgrade
- zone.js
- Don't known / other
multiple: true
validations:
+7 -23
View File
@@ -5,54 +5,38 @@ body:
- type: textarea
id: description
attributes:
label: Describe the problem that you experienced
label: Description
validations:
required: true
- type: input
id: affected-url
attributes:
label: Enter the URL of the topic with the problem
- type: textarea
id: documentation-goal
attributes:
label: Describe what you were looking for in the documentation
label: What is the affected URL?
- type: textarea
id: reproduction-steps
attributes:
label: Describe the actions that led you to experience the problem
label: Please provide the steps to reproduce the issue
- type: textarea
id: expected-vs-actual-behavior
attributes:
label: Describe what you want to experience that would fix the problem
label: Please provide the expected behavior vs the actual behavior you encountered
- type: textarea
id: screenshot
attributes:
label: Add a screenshot if that helps illustrate the problem
label: Please provide a screenshot if possible
- type: textarea
id: exception-or-error
attributes:
label: If this problem caused an exception or error, please paste it here
label: Please provide the exception or error you saw
render: true
placeholder: |
```
Paste the exception or error here inside a markdown code block,
which is annotated by three grave \(&grave;\) characters before and after the text block.
```
- type: textarea
id: browser-info
attributes:
label: If the problem is browser-specific, please specify the device, OS, browser, and version
render: true
- type: textarea
id: additional-info
attributes:
label: Provide any additional information here in as much as detail as you can
label: Is this a browser-specific issue? If so, please specify the device, browser, and version.
render: true
-54
View File
@@ -1,54 +0,0 @@
name: 'Angular DevTools'
description: Report an issue or suggest a feature for Angular DevTools
body:
- type: checkboxes
id: bug-report-or-feature-request
attributes:
label: Is this a bug report or a feature request?
description: Select one.
options:
- label: Bug Report
- label: Feature Request
- type: textarea
id: reproduction-steps
attributes:
label: Please provide the steps to reproduce the issue [Bug Report only]
- type: textarea
id: expected-vs-actual-behavior
attributes:
label: Please provide the expected behavior vs the actual behavior you encountered [Bug Report only]
- type: textarea
id: screenshot
attributes:
label: Please provide a screenshot if possible [Bug Report only]
- type: textarea
id: exception-or-error
attributes:
label: Please provide the exception or error you saw [Bug Report only]
render: true
- type: textarea
id: browser-info
attributes:
label: Is this a browser-specific issue? If so, please specify the device, browser, and version. [Bug Report only]
render: true
- type: textarea
id: description
attributes:
label: Description [Feature Request only]
- type: textarea
id: proposed-solution
attributes:
label: Proposed solution [Feature Request only]
- type: textarea
id: alternatives-considered
attributes:
label: Alternatives considered [Feature Request only]
-17
View File
@@ -1,17 +0,0 @@
name: 'Installing Yarn dependencies'
description: 'Installs the dependencies using Yarn'
runs:
using: 'composite'
steps:
- uses: actions/cache@88522ab9f39a2ea568f7027eddc7d8d8bc9d59c8 # v3
with:
path: |
./node_modules/
# If just `WORKSPACE` or `yarn.lock` is changed, the most recent cache can be restored.
# See: https://docs.github.com/en/actions/guides/caching-dependencies-to-speed-up-workflows#example-using-the-cache-action.
key: v1-angular-main-deps-${{hashFiles('WORKSPACE')}}-${{hashFiles('yarn.lock')}}
restore-keys: v1-angular-main-deps-
- run: yarn install --frozen-lockfile --non-interactive
shell: bash
+75 -17
View File
@@ -1,8 +1,8 @@
# Configuration for angular-robot
# options for the size plugin
#options for the size plugin
size:
disabled: true
disabled: false
maxSizeIncrease: 2000
circleCiStatusName: 'ci/circleci: test'
@@ -22,7 +22,7 @@ merge:
# the g3 status will be added to your pull requests if they include files that match the patterns
g3Status:
# set to true to disable
disabled: true
disabled: false
# the name of the status
context: 'google3'
# text to show when the status is pending, {{PRNumber}} will be replaced by the PR number
@@ -32,8 +32,64 @@ merge:
# link to use for the details
url: 'http://go/angular/g3sync'
# list of patterns to check for the files changed by the PR
# this list must be manually kept in sync with google3/third_party/javascript/angular2/copy.bara.sky
include:
- 'LICENSE'
- 'modules/benchmarks/**'
- 'modules/system.d.ts'
- 'packages/**'
# list of patterns to ignore for the files changed by the PR
exclude:
- 'packages/*'
- 'packages/bazel/*'
- 'packages/bazel/src/*'
- 'packages/bazel/src/api-extractor/**'
- 'packages/bazel/src/builders/**'
- 'packages/bazel/src/ng_module/**'
- 'packages/bazel/src/ng_package/**'
- 'packages/bazel/src/protractor/**'
- 'packages/bazel/src/schematics/**'
- 'packages/bazel/src/types_bundle/**'
- 'packages/compiler-cli/src/ngcc/**'
- 'packages/compiler-cli/linker/**'
- 'packages/compiler-cli/ngcc/**'
- 'packages/compiler-cli/src/ngtsc/sourcemaps/**'
# 'private' mostly contains entrypoints for 3P Angular.
# Note that 'private/migrations' _is_ used.
- 'packages/compiler-cli/private/bazel.ts'
- 'packages/compiler-cli/private/localize.ts'
- 'packages/compiler-cli/private/tooling.ts'
- 'packages/compiler-cli/private/babel.d.ts'
# google3 defines its own binary entrypoints.
- 'packages/compiler-cli/src/bin/**'
- 'packages/docs/**'
- 'packages/elements/schematics/**'
- 'packages/examples/**'
- 'packages/language-service/**'
- 'packages/localize/**'
- 'packages/private/**'
- 'packages/service-worker/**'
- 'packages/common/locales/generate-locales-tool/**'
- 'packages/common/locales/index.bzl'
- 'packages/http/**'
- '**/.gitignore'
- '**/.gitkeep'
- '**/yarn.lock'
- '**/package.json'
- '**/third_party/**'
- '**/tsconfig-build.json'
- '**/tsconfig-tsec.json'
- '**/tsconfig.json'
- '**/rollup.config.js'
- '**/BUILD.bazel'
- '**/*.md'
- 'packages/**/integrationtest/**'
- 'packages/**/test/**'
- 'packages/zone.js/*'
- 'packages/zone.js/dist/**'
- 'packages/zone.js/doc/**'
- 'packages/zone.js/example/**'
- 'packages/zone.js/scripts/**'
# comment that will be added to a PR when there is a conflict, leave empty or set to false to disable
mergeConflictComment: "Hi @{{PRAuthor}}! This PR has merge conflicts due to recent upstream merges.\nPlease help to unblock it by resolving these conflicts. Thanks!"
@@ -69,7 +125,9 @@ merge:
# list of PR statuses that need to be successful
requiredStatuses:
- 'ci/circleci: build'
- 'google-internal-tests'
- 'ci/circleci: lint'
- 'ci/angular: size'
- 'google3'
- 'pullapprove'
# the comment that will be added when the merge label is added despite failing checks, leave empty or set to false to disable
@@ -85,29 +143,29 @@ triage:
defaultMilestone: 82,
# arrays of labels that determine if an issue has been triaged by the caretaker
l1TriageLabels:
- - 'area: *'
- - 'comp: *'
# arrays of labels that determine if an issue has been fully triaged
l2TriageLabels:
- - 'P0'
- 'area: *'
- 'comp: *'
- - 'P1'
- 'area: *'
- 'comp: *'
- - 'P2'
- 'area: *'
- 'comp: *'
- - 'P3'
- 'area: *'
- 'comp: *'
- - 'P4'
- 'area: *'
- 'comp: *'
- - 'P5'
- 'area: *'
- 'comp: *'
- - 'feature'
- 'area: *'
- 'comp: *'
- - 'discussion'
- 'area: *'
- 'comp: *'
- - 'needs clarification'
- 'area: *'
- 'comp: *'
- - 'needs reproduction'
- 'area: *'
- 'comp: *'
# options for the triage PR plugin
triagePR:
@@ -119,10 +177,10 @@ triagePR:
defaultMilestone: 82,
# arrays of labels that determine if a PR has been triaged by the caretaker
l1TriageLabels:
- - 'area: *'
- - 'comp: *'
# arrays of labels that determine if a PR has been fully triaged
l2TriageLabels:
- - 'area: *'
- - 'comp: *'
# options for rerunning CI
rerunCircleCI:
-42
View File
@@ -1,42 +0,0 @@
# This workflow builds the AIO previews for pull requests when a certain label is applied.
# The actual deployment happens as part of a dedicated second workflow to avoid security
# issues where the building would otherwise occur in an authorized context where secrets
# could be leaked. More details can be found here:
# https://securitylab.github.com/research/github-actions-preventing-pwn-requests/.
name: Build AIO app for preview deployment
on:
pull_request:
types: [synchronize, labeled]
permissions: read-all
jobs:
aio-build:
runs-on: ubuntu-latest
# We only want to build and deploy the AIO app if the `aio: preview` label has been
# added, or if the label is already applied and new changes have been made in the PR.
if: |
(github.event.action == 'labeled' && github.event.label.name == 'aio: preview') ||
(github.event.action == 'synchronize' && contains(github.event.pull_request.labels.*.name, 'aio: preview'))
steps:
- uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
- uses: ./.github/actions/yarn-install
- uses: angular/dev-infra/github-actions/setup-bazel-remote-exec@97cb376945b07e37671e3b38ee727ff00dc1394b
with:
bazelrc: ./.bazelrc.user
# Build the web package. Note: We run Bazel from a low-resource Github action container,
# so we manually need to instruct Bazel to run more actions concurrently as by default
# the number of concurrent actions is determined based on the host resources.
- run: bazel build //aio:build --jobs=32 --announce_rc --verbose_failures
- uses: angular/dev-infra/github-actions/deploy-previews/pack-and-upload-artifact@97cb376945b07e37671e3b38ee727ff00dc1394b
with:
workflow-artifact-name: 'aio'
pull-number: '${{github.event.pull_request.number}}'
artifact-build-revision: '${{github.event.pull_request.head.sha}}'
deploy-directory: './dist/bin/aio/build'
-44
View File
@@ -1,44 +0,0 @@
# This workflow runs whenever the AIO build workflow has completed. Deployment happens
# as part of a dedicated second workflow to avoid security issues where the building would
# otherwise occur in an authorized context where secrets could be leaked.
#
# More details can be found here:
# https://securitylab.github.com/research/github-actions-preventing-pwn-requests/.
name: Deploying AIO preview to Firebase
on:
workflow_run:
workflows: ['Build AIO app for preview deployment']
types: [completed]
permissions:
# Needed in order to be able to comment on the pull request.
pull-requests: write
env:
PREVIEW_PROJECT: ng-dev-previews
PREVIEW_SITE: ng-dev-previews-fw
jobs:
aio-deploy:
runs-on: ubuntu-latest
if: ${{ github.event.workflow_run.conclusion == 'success' }}
steps:
- uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3
- name: Configure Firebase deploy target
working-directory: aio/
run: |
# We can use `npx` as the Firebase deploy actions uses it too.
npx -y firebase-tools@latest target:clear --project ${{env.PREVIEW_PROJECT}} hosting aio
npx -y firebase-tools@latest target:apply --project ${{env.PREVIEW_PROJECT}} hosting aio ${{env.PREVIEW_SITE}}
- uses: angular/dev-infra/github-actions/deploy-previews/upload-artifacts-to-firebase@97cb376945b07e37671e3b38ee727ff00dc1394b
with:
github-token: '${{secrets.GITHUB_TOKEN}}'
workflow-artifact-name: 'aio'
firebase-config-dir: './aio/'
firebase-public-dir: './aio/dist'
firebase-project-id: '${{env.PREVIEW_PROJECT}}'
firebase-service-key: '${{secrets.FIREBASE_PREVIEW_SERVICE_TOKEN}}'
@@ -1,21 +0,0 @@
name: DevInfra
on:
push:
pull_request_target:
types: [opened, synchronize, reopened, ready_for_review, labeled]
# Declare default permissions as read only.
permissions:
contents: read
jobs:
assistant_to_the_branch_manager:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
with:
persist-credentials: false
- uses: angular/dev-infra/github-actions/branch-manager@97cb376945b07e37671e3b38ee727ff00dc1394b
with:
angular-robot-key: ${{ secrets.ANGULAR_ROBOT_PRIVATE_KEY }}
-68
View File
@@ -1,68 +0,0 @@
name: Run benchmark comparison
on:
issue_comment:
types: [created]
permissions: read-all
jobs:
benchmark-compare:
runs-on: ubuntu-latest
if: ${{ github.event.issue.pull_request && startsWith(github.event.comment.body, '/benchmark-compare ')}}
steps:
- uses: TheModdingInquisition/actions-team-membership@a69636a92bc927f32c3910baac06bacc949c984c # v1.0
with:
team: 'team'
organization: angular
token: ${{secrets.BENCHMARK_COMPARE_MEMBERSHIP_GITHUB_TOKEN}}
exit: true
# Indicate that the benchmark command was received.
- uses: peter-evans/create-or-update-comment@c6c9a1a66007646a28c153e2a8580a5bad27bcfa # v3
with:
comment-id: ${{github.event.comment.id}}
token: '${{secrets.BENCHMARK_POST_RESULTS_GITHUB_TOKEN}}'
reactions: 'rocket'
- uses: alessbell/pull-request-comment-branch@aad01d65d6982b8eacabed5e9a684cd8ceb98da6 # v1.1
id: comment-branch
- uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
with:
# Specify repository as the PR branch might be from a fork.
repository: ${{steps.comment-branch.outputs.head_owner}}/${{steps.comment-branch.outputs.head_repo}}
# Checkout the pull request and assume it being trusted given we've checked
# that the action was triggered by a team member.
ref: ${{steps.comment-branch.outputs.head_ref}}
- uses: ./.github/actions/yarn-install
- uses: angular/dev-infra/github-actions/setup-bazel-remote-exec@97cb376945b07e37671e3b38ee727ff00dc1394b
with:
bazelrc: ./.bazelrc.user
- name: Preparing benchmark for GitHub action
id: info
env:
# Untrusted input used in an executable code, must be wrapped as an env var to prevent injections
COMMENT_BODY: ${{ github.event.comment.body }}
run: yarn benchmarks prepare-for-github-action "$COMMENT_BODY"
- run: yarn benchmarks run-compare ${{steps.info.outputs.compareSha}} ${{steps.info.outputs.benchmarkTarget}}
id: benchmark
name: Running benchmark
- uses: peter-evans/create-or-update-comment@c6c9a1a66007646a28c153e2a8580a5bad27bcfa # v3
with:
issue-number: ${{github.event.issue.number}}
token: '${{secrets.BENCHMARK_POST_RESULTS_GITHUB_TOKEN}}'
body: |
## Benchmark Test Results
**Test**: `${{steps.info.outputs.benchmarkTarget}}`
### PR (${{steps.info.outputs.prHeadSha}})
${{steps.benchmark.outputs.workingStageResultsText}}
### Compare Ref (${{steps.info.outputs.compareSha}})
${{steps.benchmark.outputs.comparisonResultsText}}
-57
View File
@@ -1,57 +0,0 @@
name: CI
on:
push:
branches:
- main
- '[0-9]+.[0-9]+.x'
pull_request:
types: [opened, synchronize, reopened]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
permissions: {}
defaults:
run:
shell: bash
jobs:
lint:
runs-on: ubuntu-latest
steps:
- name: Initialize environment
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@852c3d1898775ec9fbc87213e519f16e78805b57
with:
cache-node-modules: true
node-module-directories: |
./node_modules
./aio/node_modules
- name: Install node modules
run: yarn install --frozen-lockfile
- name: Install node modules in aio
run: yarn install --frozen-lockfile --cwd aio
- name: Check code lint
run: yarn -s tslint
- name: Check code lint in aio
run: yarn -s --cwd aio lint
- name: Check for circular dependencies
run: yarn -s ts-circular-deps:check
- name: Validate pull approve configuration
run: yarn -s ng-dev pullapprove verify
- name: Validate angular robot configuration
run: yarn -s ng-dev ngbot verify
- name: Confirm code builds with typescript as expected
run: yarn -s check-tooling-setup
- name: Check commit message
# Commit message validation is only done on pull requests as its too late to validate once
# it has been merged.
if: github.event_name == 'pull_request'
run: yarn ng-dev commit-message validate-range ${{ github.event.pull_request.base.sha }} ${{ github.event.pull_request.head.sha }}
- name: Check code format
# Code formatting checks are only done on pull requests as its too late to validate once
# it has been merged.
if: github.event_name == 'pull_request'
run: yarn ng-dev format changed --check ${{ github.event.pull_request.base.sha }}
+4 -4
View File
@@ -12,14 +12,14 @@ jobs:
labels:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
- uses: angular/dev-infra/github-actions/commit-message-based-labels@97cb376945b07e37671e3b38ee727ff00dc1394b
- uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b # tag=v3.0.2
- uses: angular/dev-infra/github-actions/commit-message-based-labels@22d3067021130271afcfd02d063828c5bdd9c2d7
with:
angular-robot-key: ${{ secrets.ANGULAR_ROBOT_PRIVATE_KEY }}
post_approval_changes:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
- uses: angular/dev-infra/github-actions/post-approval-changes@97cb376945b07e37671e3b38ee727ff00dc1394b
- uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b # tag=v3.0.2
- uses: angular/dev-infra/github-actions/post-approval-changes@22d3067021130271afcfd02d063828c5bdd9c2d7
with:
angular-robot-key: ${{ secrets.ANGULAR_ROBOT_PRIVATE_KEY }}
+1 -1
View File
@@ -14,6 +14,6 @@ jobs:
if: github.repository == 'angular/angular'
runs-on: ubuntu-latest
steps:
- uses: angular/dev-infra/github-actions/feature-request@97cb376945b07e37671e3b38ee727ff00dc1394b
- uses: angular/dev-infra/github-actions/feature-request@22d3067021130271afcfd02d063828c5bdd9c2d7
with:
angular-robot-key: ${{ secrets.ANGULAR_ROBOT_PRIVATE_KEY }}
@@ -1,20 +0,0 @@
name: Google Internal Tests Enforcement
on:
pull_request_target:
types: [opened, reopened, synchronize]
permissions:
pull-requests: read
statuses: write
jobs:
trigger:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
- uses: angular/dev-infra/github-actions/google-internal-tests@97cb376945b07e37671e3b38ee727ff00dc1394b
with:
run-tests-guide-url: http://go/angular-g3sync-start
github-token: ${{ secrets.GITHUB_TOKEN }}
sync-config: ./.ng-dev/google-sync-config.json
+19
View File
@@ -0,0 +1,19 @@
name: Lock closed inactive issues
on:
schedule:
# Run at 16:00 every day
- cron: '0 16 * * *'
# Declare default permissions as read only.
permissions:
contents: read
jobs:
lock_closed:
if: github.repository == 'angular/angular'
runs-on: ubuntu-latest
steps:
- uses: angular/dev-infra/github-actions/lock-closed@22d3067021130271afcfd02d063828c5bdd9c2d7
with:
lock-bot-key: ${{ secrets.LOCK_BOT_PRIVATE_KEY }}
-14
View File
@@ -1,14 +0,0 @@
name: Merge Ready
on: pull_request_target
# Declare default permissions as read only.
permissions: {}
jobs:
status:
runs-on: ubuntu-latest
steps:
- uses: angular/dev-infra/github-actions/unified-status-check@97cb376945b07e37671e3b38ee727ff00dc1394b
with:
angular-robot-key: ${{ secrets.ANGULAR_ROBOT_PRIVATE_KEY }}
+4 -6
View File
@@ -18,19 +18,17 @@ jobs:
permissions:
# Needed to upload the results to code-scanning dashboard.
security-events: write
# Needed to publish results
id-token: write
actions: read
contents: read
steps:
- name: 'Checkout code'
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b # tag=v3.0.2
with:
persist-credentials: false
- name: 'Run analysis'
uses: ossf/scorecard-action@08b4669551908b1024bb425080c797723083c031 # v2.2.0
uses: ossf/scorecard-action@ce330fde6b1a5c9c75b417e7efc510b822a35564 # tag=v1.1.2
with:
results_file: results.sarif
results_format: sarif
@@ -39,7 +37,7 @@ jobs:
# Upload the results as artifacts.
- name: 'Upload artifact'
uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
uses: actions/upload-artifact@3cea5372237819ed00197afe530f5a7ea3e805c8 # tag=v3.1.0
with:
name: SARIF file
path: results.sarif
@@ -47,6 +45,6 @@ jobs:
# Upload the results to GitHub's code scanning dashboard.
- name: 'Upload to code-scanning'
uses: github/codeql-action/upload-sarif@46ed16ded91731b2df79a2893d3aea8e9f03b5c4 # v2.20.3
uses: github/codeql-action/upload-sarif@2ca79b6fa8d3ec278944088b4aa5f46912db5d63 # tag=v2.1.18
with:
sarif_file: results.sarif
-44
View File
@@ -1,44 +0,0 @@
name: Update AIO Angular CLI help
on:
workflow_dispatch:
inputs: {}
push:
branches:
- 'main'
- '[0-9]+.[0-9]+.x'
# Declare default permissions as read only.
permissions:
contents: read
jobs:
update_cli_help:
name: Update Angular CLI help (if necessary)
if: github.repository == 'angular/angular'
runs-on: ubuntu-latest
steps:
- name: Checkout the repository
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
with:
# Setting `persist-credentials: false` prevents the github-action account from being the
# account that is attempted to be used for authentication, instead the remote is set to
# an authenticated URL.
persist-credentials: false
# This is needed as otherwise the PR creation will fail with `shallow update not allowed` when the forked branch is not in sync.
fetch-depth: 0
- name: Generate CLI help
run: node aio/scripts/update-cli-help/index.mjs
env:
ANGULAR_CLI_BUILDS_READONLY_GITHUB_TOKEN: ${{ secrets.ANGULAR_CLI_BUILDS_READONLY_GITHUB_TOKEN }}
- name: Create a PR (if necessary)
uses: angular/dev-infra/github-actions/create-pr-for-changes@97cb376945b07e37671e3b38ee727ff00dc1394b
with:
branch-prefix: update-cli-help
pr-title: 'docs: update Angular CLI help [${{github.ref_name}}]'
pr-description: |
Updated Angular CLI help contents.
pr-labels: |
action: review
area: docs
angular-robot-token: ${{ secrets.ANGULAR_ROBOT_ACCESS_TOKEN }}
-48
View File
@@ -1,48 +0,0 @@
# Update the `events.json` file that powers the [AIO events page](https://angular.io/events) (if
# necessary) and create a pull request.
#
# For more details on the overall process, see
# [aio/scripts/generate-events/README.md](../../aio/scripts/generate-events/README.md).
name: Update AIO events
on:
workflow_dispatch:
inputs: {}
schedule:
# Run every day at 15:00.
- cron: '0 15 * * *'
# Declare default permissions as read only.
permissions:
contents: read
jobs:
update_events:
name: Update `events.json` (if necessary)
if: github.repository == 'angular/angular'
runs-on: ubuntu-latest
steps:
- name: Checkout the repository
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
with:
# Setting `persist-credentials: false` prevents the github-action account from being the
# account that is attempted to be used for authentication, instead the remote is set to
# an authenticated URL.
persist-credentials: false
# This is needed as otherwise the PR creation will fail with `shallow update not allowed` when the forked branch is not in sync.
fetch-depth: 0
- name: Generate `events.json`
run: node aio/scripts/generate-events/index.mjs --ignore-invalid-dates
- name: Create a PR (if necessary)
uses: angular/dev-infra/github-actions/create-pr-for-changes@97cb376945b07e37671e3b38ee727ff00dc1394b
with:
branch-prefix: docs-update-events
pr-title: 'docs: update events'
pr-description: |
Generated `events.json` with the latest events retrieved from the Firebase DB.
pr-labels: |
action: review
area: docs
target: patch
angular-robot-token: ${{ secrets.ANGULAR_ROBOT_ACCESS_TOKEN }}
+1 -2
View File
@@ -4,7 +4,7 @@
/bazel-out
/integration/bazel/bazel-*
*.log
/node_modules/
node_modules
# CircleCI temporary file for cache key computation.
# See `save_month_to_file` in `.circleci/config.yml`.
@@ -56,4 +56,3 @@ baseline.json
# Husky
.husky/_
aio/content/examples/.DS_Store
+2 -3
View File
@@ -2,7 +2,6 @@ import {CaretakerConfig} from '@angular/ng-dev';
/** The configuration for `ng-dev caretaker` commands. */
export const caretaker: CaretakerConfig = {
g3SyncConfigPath: './.ng-dev/google-sync-config.json',
githubQueries: [
{
name: 'Merge Queue',
@@ -10,11 +9,11 @@ export const caretaker: CaretakerConfig = {
},
{
name: 'Merge Assistance Queue',
query: `is:pr is:open label:"merge: caretaker note" label:"action: merge"`,
query: `is:pr is:open label:"action: merge-assistance"`,
},
{
name: 'Initial Triage Queue',
query: `is:open no:milestone -draft:true`,
query: `is:open no:milestone`,
},
],
caretakerGroup: 'angular-caretaker',
+3
View File
@@ -25,10 +25,13 @@ export const commitMessage: CommitMessageConfig = {
'language-service',
'localize',
'migrations',
'ngcc',
'packaging',
'platform-browser',
'platform-browser-dynamic',
'platform-server',
'platform-webworker',
'platform-webworker-dynamic',
'router',
'service-worker',
'upgrade',
-1
View File
@@ -8,5 +8,4 @@ export const github: GithubConfig = {
owner: 'angular',
name: 'angular',
mainBranchName: 'main',
useNgDevAuthService: true,
};
-51
View File
@@ -1,51 +0,0 @@
{
"syncedFilePatterns": ["LICENSE", "modules/benchmarks/**", "packages/**"],
"alwaysExternalFilePatterns": [
"packages/*",
"packages/bazel/*",
"packages/bazel/src/*",
"packages/bazel/src/api-extractor/**",
"packages/bazel/src/builders/**",
"packages/bazel/src/ng_module/**",
"packages/bazel/src/ng_package/**",
"packages/bazel/src/protractor/**",
"packages/bazel/src/schematics/**",
"packages/bazel/src/types_bundle/**",
"packages/compiler-cli/linker/**",
"packages/compiler-cli/src/ngtsc/sourcemaps/**",
"packages/compiler-cli/src/ngtsc/testing/**",
"packages/compiler-cli/private/bazel.ts",
"packages/compiler-cli/private/localize.ts",
"packages/compiler-cli/private/tooling.ts",
"packages/compiler-cli/private/babel.d.ts",
"packages/compiler-cli/src/bin/**",
"packages/docs/**",
"packages/elements/schematics/**",
"packages/examples/**",
"packages/language-service/**",
"packages/localize/**",
"packages/private/**",
"packages/service-worker/**",
"packages/common/locales/generate-locales-tool/**",
"packages/common/locales/index.bzl",
"packages/http/**",
"**/.gitignore",
"**/.gitkeep",
"**/yarn.lock",
"**/package.json",
"**/third_party/**",
"**/tsconfig-build.json",
"**/tsconfig-tsec.json",
"**/tsconfig.json",
"**/rollup.config.js",
"**/BUILD.bazel",
"**/*.md",
"packages/**/integrationtest/**",
"packages/**/test/**",
"packages/zone.js/*",
"packages/zone.js/dist/**",
"packages/zone.js/doc/**",
"packages/zone.js/example/**",
"packages/zone.js/scripts/**"
]
}
+3
View File
@@ -6,6 +6,9 @@ import {PullRequestConfig} from '@angular/ng-dev';
*/
export const pullRequest: PullRequestConfig = {
githubApiMerge: false,
mergeReadyLabel: 'action: merge',
caretakerNoteLabel: 'action: merge-assistance',
commitMessageFixupLabel: 'commit message fixup',
requiredBaseCommits: {
// PRs that target either `main` or the patch branch, need to be rebased
// on top of the latest commit message validation fix.
+2 -1
View File
@@ -6,6 +6,7 @@ export const release: ReleaseConfig = {
representativeNpmPackage: '@angular/core',
npmPackages: [
{name: '@angular/animations'},
{name: '@angular/bazel'},
{name: '@angular/common'},
{name: '@angular/compiler'},
{name: '@angular/compiler-cli'},
@@ -30,5 +31,5 @@ export const release: ReleaseConfig = {
releaseNotes: {
hiddenScopes: ['aio', 'bazel', 'dev-infra', 'docs-infra', 'zone.js', 'devtools'],
},
releasePrLabels: ['area: build & ci', 'action: merge', 'PullApprove: disable'],
releasePrLabels: ['comp: build & ci', 'action: merge', 'PullApprove: disable'],
};
+1 -1
View File
@@ -1 +1 @@
16.14.0
16.10.0
+41 -80
View File
@@ -53,12 +53,18 @@
# @angular/framework-global-approvers-for-docs-only-changes
# =========================================================
# Used for approving minor documentation-only changes that don't require engineering review.
#
# gkalpak
# josmar-crwdstffng
# ileil
# TeriGlover
# bob-watson
version: 3
availability:
users_unavailable:
- atscott
- atscott # August 15 - September 20 (at least)
# Meta field that goes unused by PullApprove to allow for defining aliases to be
# used throughout the config.
@@ -123,18 +129,12 @@ groups:
'packages/animations/**/{*,.*}',
'packages/platform-browser/animations/**/{*,.*}',
'aio/content/guide/animations.md',
'aio/content/guide/add-an-animation.md',
'aio/content/guide/animate-state-style.md',
'aio/content/guide/animation-api-summary.md',
'aio/content/guide/understanding-angular-animation.md',
'aio/content/examples/animations/**/{*,.*}',
'aio/content/images/guide/animations/**/{*,.*}',
'aio/content/guide/complex-animation-sequences.md',
'aio/content/guide/reusable-animations.md',
'aio/content/guide/route-animations.md',
'aio/content/guide/transition-and-triggers.md',
'aio/content/guide/animations-attach-to-html-template.md',
'aio/content/guide/animation-transition-timing.md'
'aio/content/guide/transition-and-triggers.md'
])
reviewers:
users:
@@ -150,7 +150,7 @@ groups:
<<: *defaults
conditions:
- >
contains_any_globs(files, [
contains_any_globs(files.exclude('packages/compiler-cli/ngcc/*'), [
'packages/compiler/**/{*,.*}',
'packages/examples/compiler/**/{*,.*}',
'aio/content/examples/angular-compiler-options/**/{*,.*}',
@@ -158,15 +158,26 @@ groups:
'aio/content/guide/angular-compiler-options.md',
'aio/content/guide/aot-compiler.md',
'aio/content/guide/aot-metadata-errors.md',
'aio/content/guide/template-typecheck.md',
'aio/content/extended-diagnostics/*.md'
'aio/content/guide/template-typecheck.md'
])
reviewers:
users:
- alxhub
- AndrewKushnir
- atscott
- crisbeto
- JoostK
# =========================================================
# Framework: Compiler / ngcc
# =========================================================
fw-ngcc:
<<: *defaults
conditions:
- files.include('packages/compiler-cli/ngcc/*')
reviewers:
users:
- alxhub
- gkalpak
- JoostK
# =========================================================
@@ -205,7 +216,6 @@ groups:
'aio/content/guide/architecture-services.md',
'aio/content/guide/architecture.md',
'aio/content/examples/architecture/**/{*,.*}',
'aio/content/guide/rxjs-interop.md',
'aio/content/images/guide/architecture/**/{*,.*}',
'aio/content/guide/attribute-directives.md',
'aio/content/examples/attribute-directives/**/{*,.*}',
@@ -223,8 +233,6 @@ groups:
'aio/content/examples/component-interaction/**/{*,.*}',
'aio/content/images/guide/component-interaction/**/{*,.*}',
'aio/content/guide/component-overview.md',
'aio/content/errors/*.md',
'aio/content/examples/errors/**/{*,.*}',
'aio/content/examples/component-overview/**/{*,.*}',
'aio/content/guide/component-styles.md',
'aio/content/guide/developer-guide-overview.md',
@@ -242,7 +250,6 @@ groups:
'aio/content/images/guide/dependency-injection-in-action/**/{*,.*}',
'aio/content/guide/dependency-injection-navtree.md',
'aio/content/guide/dependency-injection-providers.md',
'aio/content/guide/dependency-injection-context.md',
'aio/content/guide/lightweight-injection-tokens.md',
'aio/content/guide/displaying-data.md',
'aio/content/examples/displaying-data/**/{*,.*}',
@@ -251,6 +258,7 @@ groups:
'aio/content/examples/dynamic-component-loader/**/{*,.*}',
'aio/content/images/guide/dynamic-component-loader/**/{*,.*}',
'aio/content/guide/example-apps-list.md',
'aio/content/guide/entry-components.md',
'aio/content/guide/feature-modules.md',
'aio/content/examples/feature-modules/**/{*,.*}',
'aio/content/images/guide/feature-modules/**/{*,.*}',
@@ -258,8 +266,6 @@ groups:
'aio/content/images/guide/frequent-ngmodules/**/{*,.*}',
'aio/content/guide/hierarchical-dependency-injection.md',
'aio/content/examples/hierarchical-dependency-injection/**/{*,.*}',
'aio/content/guide/image-directive.md',
'aio/content/guide/image-directive-setup.md',
'aio/content/examples/providers-viewproviders/**/{*,.*}',
'aio/content/examples/resolution-modifiers/**/{*,.*}',
'aio/content/guide/lazy-loading-ngmodules.md',
@@ -297,7 +303,6 @@ groups:
'aio/content/guide/two-way-binding.md',
'aio/content/examples/two-way-binding/**/{*,.*}',
'aio/content/guide/built-in-directives.md',
'aio/content/guide/directive-composition-api.md',
'aio/content/examples/built-in-directives/**/{*,.*}',
'aio/content/images/guide/built-in-directives/**/{*,.*}',
'aio/content/guide/template-reference-variables.md',
@@ -321,7 +326,6 @@ groups:
'aio/content/guide/singleton-services.md',
'aio/content/guide/sharing-ngmodules.md',
'aio/content/guide/standalone-components.md',
'aio/content/guide/standalone-migration.md',
'aio/content/guide/structural-directives.md',
'aio/content/examples/structural-directives/**/{*,.*}',
'aio/content/guide/svg-in-templates.md',
@@ -335,16 +339,13 @@ groups:
'aio/content/guide/view-encapsulation.md',
'aio/content/examples/view-encapsulation/**/{*,.*}',
'aio/content/images/guide/view-encapsulation/**/{*,.*}',
'aio/content/special-elements/**/{*,.*}',
'aio/content/guide/hydration.md',
'aio/content/guide/signals.md'
'aio/content/special-elements/**/{*,.*}'
])
reviewers:
users:
- alxhub
- AndrewKushnir
- atscott
- crisbeto
- dylhunn
- jessicajaniuk
- pkozlowski-opensource
@@ -379,21 +380,7 @@ groups:
contains_any_globs(files, [
'packages/common/http/**/{*,.*}',
'packages/examples/http/**/{*,.*}',
'aio/content/guide/understanding-communicating-with-http.md',
'aio/content/guide/http-configure-http-url-parameters.md',
'aio/content/guide/http-handle-request-errors.md',
'aio/content/guide/http-intercept-requests-and-responses.md',
'aio/content/guide/http-interceptor-use-cases.md',
'aio/content/guide/http-make-jsonp-request.md',
'aio/content/guide/http-optimize-server-interaction.md',
'aio/content/guide/http-pass-metadata-to-interceptors.md',
'aio/content/guide/http-request-data-from-server.md',
'aio/content/guide/http-security-xsrf-protection.md',
'aio/content/guide/http-send-data-to-server.md',
'aio/content/guide/http-server-communication.md',
'aio/content/guide/http-setup-server-communication.md',
'aio/content/guide/http-test-requests.md',
'aio/content/guide/http-track-show-request-progress.md',
'aio/content/guide/http.md',
'aio/content/examples/http/**/{*,.*}',
'aio/content/images/guide/http/**/{*,.*}'
])
@@ -426,6 +413,7 @@ groups:
- andrewseguin
- atscott
- dylhunn
- gkalpak
- jessicajaniuk
- pkozlowski-opensource
@@ -576,6 +564,7 @@ groups:
reviewers:
users:
- alxhub
- gkalpak
# =========================================================
# Framework: Upgrade
@@ -603,6 +592,7 @@ groups:
reviewers:
users:
- alxhub
- gkalpak
- jessicajaniuk
# =========================================================
@@ -781,9 +771,8 @@ groups:
])
reviewers:
users:
- devversion
- alxhub
- josephperrott
- pkozlowski-opensource
# =========================================================
# Integration Tests
@@ -834,9 +823,6 @@ groups:
'aio/content/images/guide/setup-local/**/{*,.*}',
'aio/content/tutorial/**/{*,.*}',
'aio/content/images/guide/toh/**/{*,.*}',
'aio/content/demos/first-app/**/{*,.*}',
'aio/content/examples/first-app-*/**/{*,.*}',
'aio/content/images/guide/faa/**/{*,.*}',
'aio/content/examples/toh-pt0/**/{*,.*}',
'aio/content/examples/toh-pt1/**/{*,.*}',
'aio/content/examples/toh-pt2/**/{*,.*}',
@@ -847,7 +833,6 @@ groups:
'aio/content/examples/getting-started-v0/**/{*,.*}',
'aio/content/examples/getting-started/**/{*,.*}',
'aio/content/start/**/{*,.*}',
'aio/content/guide/quick-start.md',
'aio/content/images/guide/start/**/{*,.*}',
'aio/content/examples/what-is-angular/**/{*,.*}',
'aio/content/guide/what-is-angular.md'
@@ -865,7 +850,6 @@ groups:
conditions:
- >
contains_any_globs(files.exclude("aio/content/marketing/contributors.json"), [
'aio/content/examples/marketing-first-app/**/{*,.*}',
'aio/content/guide/roadmap.md',
'aio/content/marketing/**/{*,.*}',
'aio/content/images/marketing/**/{*,.*}',
@@ -929,10 +913,6 @@ groups:
'aio/content/guide/migration-module-with-providers.md',
'aio/content/guide/static-query-migration.md',
'aio/content/guide/update-to-latest-version.md',
'aio/content/guide/versions.md',
'aio/content/guide/update-to-version-14.md',
'aio/content/guide/update-to-version-15.md',
'aio/content/guide/update-to-version-16.md',
])
reviewers:
users:
@@ -1006,7 +986,6 @@ groups:
'aio/content/guide/workspace-config.md',
'aio/content/guide/migration-solution-style-tsconfig.md',
'aio/content/guide/migration-update-libraries-tslib.md',
'aio/content/guide/esbuild.md',
])
reviewers:
users:
@@ -1072,7 +1051,6 @@ groups:
'aio/tests/**/{*,.*}',
'aio/tools/**/{*,.*}',
'aio/tools/examples/shared/boilerplate/cli/.vscode/**/{*,.*}',
'aio/content/BUILD.bazel',
'aio/content/images/guide/contributors-guide/**/{*,.*}',
'aio/content/guide/contributors-guide-overview.md',
'aio/content/guide/docs-style-guide.md',
@@ -1085,32 +1063,11 @@ groups:
'aio/content/guide/reviewing-content.md',
'aio/content/guide/updating-content-github-ui.md',
'aio/content/guide/updating-search-keywords.md',
'aio/content/guide/doc-build-test.md',
'aio/content/guide/doc-edit-finish.md',
'aio/content/guide/doc-editing.md',
'aio/content/guide/doc-github-tasks.md',
'aio/content/guide/doc-pr-open.md',
'aio/content/guide/doc-pr-prep.md',
'aio/content/guide/doc-pr-update.md',
'aio/content/guide/doc-prepare-to-edit.md',
'aio/content/guide/doc-select-issue.md',
'aio/content/guide/doc-tasks.md',
'aio/content/guide/doc-update-overview.md',
'aio/content/guide/doc-update-start.md',
'aio/content/guide/docs-lint-errors.md',
'aio/content/images/guide/preparing-to-edit-docs/**/{*,.*}',
'aio/content/images/guide/github-for-docs/**/{*,.*}',
'aio/content/images/guide/doc-update-start/**/{*,.*}',
'aio/content/images/guide/doc-update-overview/**/{*,.*}',
'aio/content/images/guide/doc-pr-update/**/{*,.*}',
'aio/content/images/guide/doc-pr-prep/**/{*,.*}',
'aio/content/images/guide/doc-pr-open/**/{*,.*}',
'aio/content/images/guide/doc-github-tasks/**/{*,.*}',
'aio/content/images/guide/doc-prepare-to-edit/**/{*,.*}',
])
reviewers:
users:
- AndrewKushnir
- gkalpak
- jessicajaniuk
- josephperrott
@@ -1126,7 +1083,7 @@ groups:
])
reviewers:
users:
- AleksanderBodurri
- aleksanderbodurri
- devversion
- josephperrott
- mgechev
@@ -1139,7 +1096,7 @@ groups:
<<: *defaults
conditions:
- >
contains_any_globs(files.exclude('.pullapprove.yml'), [
contains_any_globs(files, [
'{*,.*}',
'.circleci/**/{*,.*}',
'.devcontainer/**/{*,.*}',
@@ -1162,12 +1119,10 @@ groups:
'tools/bazel-repo-patches/**/{*,.*}',
'tools/circular_dependency_test/**/{*,.*}',
'tools/contributing-stats/**/{*,.*}',
'tools/esm-interop/**/{*,.*}',
'tools/gulp-tasks/**/{*,.*}',
'tools/legacy-saucelabs/**/{*,.*}',
'tools/rxjs/**/{*,.*}',
'tools/saucelabs/**/{*,.*}',
'tools/saucelabs-daemon/**/{*,.*}',
'tools/symbol-extractor/**/{*,.*}',
'tools/testing/**/{*,.*}',
'tools/tslint/**/{*,.*}',
@@ -1179,6 +1134,7 @@ groups:
reviewers:
users:
- devversion
- gkalpak
- josephperrott
# =========================================================
@@ -1194,8 +1150,11 @@ groups:
'goldens/public-api/**/{*,.*}',
'docs/NAMING.md',
'aio/content/guide/angular-package-format.md',
'aio/content/errors/*.md',
'aio/content/extended-diagnostics/*.md',
'aio/content/guide/glossary.md',
'aio/content/guide/styleguide.md',
'aio/content/examples/errors/**/{*,.*}',
'aio/content/examples/styleguide/**/{*,.*}',
'aio/content/images/guide/styleguide/{*,.*}'
])
@@ -1282,7 +1241,6 @@ groups:
- andrewseguin
- dgp1130
- ~jelbourn
- jessicajaniuk
- josephperrott
####################################################################################
@@ -1363,7 +1321,7 @@ groups:
reviewers:
users:
- alan-agius4 # Alan Agius
- AleksanderBodurri # Aleksander Bodurri
- aleksanderbodurri # Aleksander Bodurri
- alxhub # Alex Rickabaugh
- AndrewKushnir # Andrew Kushnir
- andrewseguin # Andrew Seguin
@@ -1373,6 +1331,8 @@ groups:
- devversion # Paul Gschwendtner
- dgp1130 # Doug Parker
- dylhunn # Dylan Hunn
- filipesilva # Filipe Silva
- gkalpak # Georgios Kalpakas
- jelbourn # Jeremy Elbourn
- jessicajaniuk # Jessica Janiuk
- JiaLiPassion # Jia Li
@@ -1382,5 +1342,6 @@ groups:
- mgechev # Minko Gechev
- mmalerba # Miles Malerba
- pkozlowski-opensource # Pawel Kozlowski
- Splaktar # Michael Prentice
- twerske # Emma Twersky
- zarend # Zach Arend
+21 -9
View File
@@ -16,7 +16,7 @@
"remoteRoot": "${workspaceRoot}",
"stopOnEntry": false,
"timeout": 600000,
"outFiles": ["${workspaceFolder}/dist/out/**/packages/**/*.js"]
"outFiles": ["${workspaceFolder}/bazel-out/**/angular/**/*.js"],
},
{
"name": "Attach to bazel test ... --config=debug (no source maps)",
@@ -30,46 +30,58 @@
"remoteRoot": "${workspaceRoot}",
"stopOnEntry": false,
"timeout": 600000,
"outFiles": ["${workspaceFolder}/dist/out/**/packages/**/*.js"]
"outFiles": ["${workspaceFolder}/bazel-out/**/angular/**/*.js"],
},
{
"name": "IVY:packages/core/test/acceptance",
"type": "node",
"request": "launch",
"program": "${workspaceFolder}/node_modules/.bin/bazelisk",
"args": ["test", "packages/core/test/acceptance", "--config=debug"],
"args": [
"test",
"packages/core/test/acceptance",
"--config=debug"
],
"port": 9229,
"address": "localhost",
"restart": true,
"sourceMaps": true,
"timeout": 600000,
"outFiles": ["${workspaceFolder}/dist/out/**/packages/**/*.js"]
"outFiles": ["${workspaceFolder}/bazel-out/**/angular/**/*.js"],
},
{
"name": "IVY:packages/core/test/render3",
"type": "node",
"request": "launch",
"program": "${workspaceFolder}/node_modules/.bin/bazelisk",
"args": ["test", "packages/core/test/render3", "--config=debug"],
"args": [
"test",
"packages/core/test/render3",
"--config=debug"
],
"port": 9229,
"address": "localhost",
"restart": true,
"sourceMaps": true,
"timeout": 600000,
"outFiles": ["${workspaceFolder}/dist/out/**/packages/**/*.js"]
"outFiles": ["${workspaceFolder}/bazel-out/**/angular/**/*.js"],
},
{
"name": "IVY:packages/core/test",
"type": "node",
"request": "launch",
"program": "${workspaceFolder}/node_modules/.bin/bazelisk",
"args": ["test", "packages/core/test", "--config=debug"],
"args": [
"test",
"packages/core/test",
"--config=debug"
],
"port": 9229,
"address": "localhost",
"restart": true,
"sourceMaps": true,
"timeout": 600000,
"outFiles": ["${workspaceFolder}/dist/out/**/packages/**/*.js"]
}
"outFiles": ["${workspaceFolder}/bazel-out/**/angular/**/*.js"],
},
]
}
+147520
View File
File diff suppressed because one or more lines are too long
-147529
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -2,4 +2,4 @@
# yarn lockfile v1
yarn-path ".yarn/releases/yarn-1.22.19.cjs"
yarn-path ".yarn/releases/yarn-1.22.17.cjs"
+13 -29
View File
@@ -1,6 +1,4 @@
load("//tools:defaults.bzl", "nodejs_binary")
load("@build_bazel_rules_nodejs//:index.bzl", "js_library")
load("//:yarn.bzl", "YARN_PATH")
package(default_visibility = ["//visibility:public"])
@@ -8,30 +6,28 @@ exports_files([
"LICENSE",
"karma-js.conf.js",
"browser-providers.conf.js",
YARN_PATH,
"scripts/ci/bazel-payload-size.sh",
"scripts/ci/payload-size.sh",
"scripts/ci/payload-size.js",
"package.json",
])
filegroup(
# filegroup is needed so that this package.json file can be a dep
# in a js_library target, which doesn't allow source file deps
name = "package_json",
srcs = ["package.json"],
)
alias(
name = "tsconfig.json",
actual = "//packages:tsconfig-build.json",
)
js_library(
name = "browser-providers",
filegroup(
name = "web_test_bootstrap_scripts",
# do not sort
srcs = [
"browser-providers.conf.d.ts",
"browser-providers.conf.js",
"@npm//:node_modules/core-js-bundle/index.js",
"//packages/zone.js/bundles:zone.umd.js",
"//packages/zone.js/bundles:zone-testing.umd.js",
"//packages/zone.js/bundles:task-tracking.umd.js",
# Including systemjs because it defines `__eval`, which produces correct stack traces.
"@npm//:node_modules/systemjs/dist/system.src.js",
"@npm//:node_modules/reflect-metadata/Reflect.js",
],
)
@@ -63,19 +59,7 @@ config_setting(
nodejs_binary(
name = "yarn_vendored",
data = [".yarn/releases/yarn-1.22.19.cjs"],
entry_point = ".yarn/releases/yarn-1.22.19.cjs",
visibility = [
"//aio/tools/examples/shared:__pkg__",
"//integration:__subpackages__",
],
)
alias(
name = "sauce_connect",
actual = select({
"@npm//@angular/build-tooling/bazel/constraints:linux_x64": "@sauce_connect_linux_amd64//:bin/sc",
"@npm//@angular/build-tooling/bazel/constraints:macos_x64": "@sauce_connect_mac//:bin/sc",
"@npm//@angular/build-tooling/bazel/constraints:macos_arm64": "@sauce_connect_mac//:bin/sc",
}),
data = [".yarn/releases/yarn-1.22.17.cjs"],
entry_point = ".yarn/releases/yarn-1.22.17.cjs",
visibility = ["//integration:__subpackages__"],
)
+2621 -1551
View File
File diff suppressed because it is too large Load Diff
-2618
View File
File diff suppressed because it is too large Load Diff
+7 -74
View File
@@ -1,79 +1,12 @@
# Contributor Covenant Code of Conduct
# Contributor Code of Conduct
## Version 0.3b-angular
## Our Pledge
As contributors and maintainers of the Angular project, we pledge to respect everyone who contributes by posting issues, updating documentation, submitting pull requests, providing feedback in comments, and any other activities.
In the interest of fostering a safe and welcoming environment, we as
the Angular team pledge to make participation in our project and
our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, sex characteristics, gender identity, gender expression,
level of experience, education, socio-economic status, nationality, personal
appearance, race, religion, or sexual identity and orientation.
Communication through any of Angular's channels (GitHub, Discord, Gitter, IRC, mailing lists, Twitter, etc.) must be constructive and never resort to personal attacks, trolling, public or private harassment, insults, or other unprofessional conduct.
## Our Standards
We promise to extend courtesy and respect to everyone involved in this project regardless of gender, gender identity, sexual orientation, disability, age, race, ethnicity, religion, or level of experience. We expect anyone contributing to the Angular project to do the same.
Examples of behavior that contributes to creating a positive environment
include:
If any member of the community violates this code of conduct, the maintainers of the Angular project may take action, removing issues, comments, and PRs or blocking accounts as deemed appropriate.
* Use welcoming and inclusive language
* Respect each other
* Provide and gracefully accept constructive criticism
* Show empathy towards other community members
Examples of unacceptable behavior by participants include:
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic
address, without explicit permission
* The use of sexualized language or imagery
* Unwelcome sexual attention or advances
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Our Responsibilities
Angular team are responsible for clarifying the standards of acceptable
behavior and are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behavior.
Angular team have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct, and to ban temporarily or
permanently any contributor for other behaviors that they deem inappropriate,
threatening, offensive, or harmful.
## Scope
This Code of Conduct applies to all Angular communication channels - online or in person,
and it also applies when an individual is representing the project or its community in
public spaces. Examples of representing a project or community include using an official
project e-mail address, posting via an official social media account, or acting
as an appointed representative at an online or offline event. Representation of
a project may be further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the Angular team at conduct@angular.io. All
complaints will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. The Angular team
will maintain confidentiality with regard to the reporter of an incident.
Enforcement may result in an indefinite ban from all official Angular communication
channels, or other actions as deemed appropriate by the Angular team.
Angular maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other
members of the project's leadership.
### Appeal
If you are banned you may contest the decision. To do so email conduct@angular.io with the subject line "Repeal Ban for {{your name here}}" and body with the responses to the following:
* Why do you believe you did not violate the Code of Conduct?
* Were other factors involved in this situation the leadership team may have been unaware of?
* Why do you wish to be a part of the Angular community?
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org), version 1.4,
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
If you are subject to or witness unacceptable behavior, or have any other concerns, please email us at [conduct@angular.io](mailto:conduct@angular.io).
+6 -2
View File
@@ -68,7 +68,7 @@ A minimal reproduction allows us to quickly confirm a bug (or point out a coding
We require a minimal reproduction to save maintainers' time and ultimately be able to fix more bugs.
Often, developers find coding problems themselves while preparing a minimal reproduction.
We understand that sometimes it might be hard to extract essential bits of code from a larger codebase, but we really need to isolate the problem before we can fix it.
We understand that sometimes it might be hard to extract essential bits of code from a larger codebase but we really need to isolate the problem before we can fix it.
Unfortunately, we are not able to investigate / fix bugs without a minimal reproduction, so if we don't hear back from you, we are going to close an issue that doesn't have enough info to be reproduced.
@@ -246,7 +246,7 @@ The `footer` is optional. The [Commit Message Footer](#commit-footer) format des
│ └─⫸ 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|docs-infra|migrations|
│ upgrade|zone.js|packaging|changelog|docs-infra|migrations|ngcc|ve|
│ devtools
│
└─⫸ Commit Type: build|ci|docs|feat|fix|perf|refactor|test
@@ -306,6 +306,10 @@ There are currently a few exceptions to the "use package name" rule:
* `migrations`: used for changes to the `ng update` migrations.
* `ngcc`: used for changes to the [Angular Compatibility Compiler](./packages/compiler-cli/ngcc/README.md)
* `ve`: used for changes specific to ViewEngine (legacy compiler/renderer).
* `devtools`: used for changes in the [browser extension](./devtools/README.md).
* none/empty string: useful for `test` and `refactor` changes that are done across all packages (e.g. `test: add missing unit tests`) and for docs changes that are not related to a specific package (e.g. `docs: fix typo in tutorial`).
+1 -1
View File
@@ -1,6 +1,6 @@
The MIT License
Copyright (c) 2010-2023 Google LLC. https://angular.io/license
Copyright (c) 2010-2022 Google LLC. https://angular.io/license
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
+5 -5
View File
@@ -1,10 +1,10 @@
<h1 align="center">Angular - The modern web developer's platform</h1>
<h1 align="center">Angular - The modern web developer's platform.</h1>
<p align="center">
<img src="aio/src/assets/images/logos/angular/angular.png" alt="angular-logo" width="120px" height="120px"/>
<br>
<em>Angular is a development platform for building mobile and desktop web applications
<br> using TypeScript/JavaScript and other languages.</em>
<i>Angular is a development platform for building mobile and desktop web applications
<br> using Typescript/JavaScript and other languages.</i>
<br>
</p>
@@ -115,11 +115,11 @@ Check out our [upgrade guide](https://update.angular.io/) to find out the best w
### Contributing Guidelines
Read through our [contributing guidelines][contributing] to learn about our submission process, coding rules, and more.
Read through our [contributing guidelines][contributing] to learn about our submission process, coding rules and more.
### Want to Help?
Want to report a bug, contribute some code, or improve the documentation? Excellent! Read up on our guidelines for [contributing][contributing] and then check out one of our issues labeled as <kbd>[help wanted](https://github.com/angular/angular/labels/help%20wanted)</kbd> or <kbd>[good first issue](https://github.com/angular/angular/labels/good%20first%20issue)</kbd>.
Want to report a bug, contribute some code, or improve documentation? Excellent! Read up on our guidelines for [contributing][contributing] and then check out one of our issues labeled as <kbd>[help wanted](https://github.com/angular/angular/labels/help%20wanted)</kbd> or <kbd>[good first issue](https://github.com/angular/angular/labels/good%20first%20issue)</kbd>.
### Code of Conduct
-3
View File
@@ -1,3 +0,0 @@
Angular is part of Google [Open Source Software Vulnerability Reward Program](https://bughunters.google.com/about/rules/6521337925468160/google-open-source-software-vulnerability-reward-program-rules). For vulnerabilities in Angular, please submit your report [here](https://bughunters.google.com/report).
For more information, check out [Angular's security policy](https://angular.io/guide/security).
+16 -82
View File
@@ -7,7 +7,6 @@ workspace(
)
load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
load("//:yarn.bzl", "YARN_LABEL")
# Add a patch fix for rules_webtesting v0.3.5 required for enabling runfiles on Windows.
# TODO: Remove the http_archive for this transitive dependency when a release is cut
@@ -25,11 +24,8 @@ http_archive(
http_archive(
name = "build_bazel_rules_nodejs",
patches = [
"//tools/esm-interop:patches/bazel/nodejs_binary_esm_support.patch",
],
sha256 = "5dd1e5dea1322174c57d3ca7b899da381d516220793d0adef3ba03b9d23baa8e",
urls = ["https://github.com/bazelbuild/rules_nodejs/releases/download/5.8.3/rules_nodejs-5.8.3.tar.gz"],
sha256 = "c78216f5be5d451a42275b0b7dc809fb9347e2b04a68f68bad620a2b01f5c774",
urls = ["https://github.com/bazelbuild/rules_nodejs/releases/download/5.5.2/rules_nodejs-5.5.2.tar.gz"],
)
load("@build_bazel_rules_nodejs//:repositories.bzl", "build_bazel_rules_nodejs_dependencies")
@@ -38,8 +34,6 @@ build_bazel_rules_nodejs_dependencies()
# The PKG rules are needed to build tar packages for integration tests. The builtin
# rule in `@bazel_tools` is not Windows compatible and outdated.
# NOTE: We cannot move past version 0.6.0 as pkg_tar no longer works on directories, which rules_nodejs
# relies on for node_modules setup.
http_archive(
name = "rules_pkg",
sha256 = "62eeb544ff1ef41d786e329e1536c1d541bb9bcad27ae984d57f18f314018e66",
@@ -50,13 +44,11 @@ http_archive(
)
# Fetch Aspect lib for utilities like write_source_files
# NOTE: We cannot move past version 1.23.2 of aspect_bazel_lib because it requires us to move to bazel 6.0.0 which
# breaks our usage of managed_directories
http_archive(
name = "aspect_bazel_lib",
sha256 = "4b2e774387bae6242879820086b7b738d49bf3d0659522ea5d9363be01a27582",
strip_prefix = "bazel-lib-1.23.2",
url = "https://github.com/aspect-build/bazel-lib/archive/refs/tags/v1.23.2.tar.gz",
sha256 = "5f5f1237601d41d61608ad0b9541614935839232940010f9e62163c3e53dc1b7",
strip_prefix = "bazel-lib-0.5.0",
url = "https://github.com/aspect-build/bazel-lib/archive/refs/tags/v0.5.0.tar.gz",
)
# Setup the Node.js toolchain.
@@ -64,12 +56,7 @@ load("@rules_nodejs//nodejs:repositories.bzl", "nodejs_register_toolchains")
nodejs_register_toolchains(
name = "nodejs",
node_version = "16.14.0",
)
nodejs_register_toolchains(
name = "node18",
node_version = "18.10.0",
node_version = "16.10.0",
)
# Download npm dependencies.
@@ -81,14 +68,11 @@ yarn_install(
# Note that we add the postinstall scripts here so that the dependencies are re-installed
# when the postinstall patches are modified.
data = [
YARN_LABEL,
"//:.yarn/releases/yarn-1.22.17.cjs",
"//:.yarnrc",
"//:scripts/puppeteer-chromedriver-versions.js",
"//:scripts/webdriver-manager-update.js",
"//tools:postinstall-patches.js",
"//tools/esm-interop:patches/npm/@angular+build-tooling+0.0.0-8d4803573edc70b90a1134ffa996303d1dcc18a9.patch",
"//tools/esm-interop:patches/npm/@bazel+concatjs+5.8.1.patch",
"//tools/esm-interop:patches/npm/@bazel+esbuild+5.7.1.patch",
"//tools/esm-interop:patches/npm/@bazel+protractor+5.7.1.patch",
"//tools/esm-interop:patches/npm/rxjs+6.6.7.patch",
],
# Currently disabled due to:
# 1. Missing Windows support currently.
@@ -99,7 +83,7 @@ yarn_install(
# We prefer to symlink the `node_modules` to only maintain a single install.
# See https://github.com/angular/dev-infra/pull/446#issuecomment-1059820287 for details.
symlink_node_modules = True,
yarn = YARN_LABEL,
yarn = "//:.yarn/releases/yarn-1.22.17.cjs",
yarn_lock = "//:yarn.lock",
)
@@ -108,9 +92,8 @@ yarn_install(
# Note that we add the postinstall scripts here so that the dependencies are re-installed
# when the postinstall patches are modified.
data = [
YARN_LABEL,
"//:.yarn/releases/yarn-1.22.17.cjs",
"//:.yarnrc",
"//aio:tools/cli-patches/bazel-architect-output.patch",
"//aio:tools/cli-patches/patch.js",
],
# Currently disabled due to:
@@ -122,34 +105,10 @@ yarn_install(
# We prefer to symlink the `node_modules` to only maintain a single install.
# See https://github.com/angular/dev-infra/pull/446#issuecomment-1059820287 for details.
symlink_node_modules = True,
yarn = YARN_LABEL,
yarn = "//:.yarn/releases/yarn-1.22.17.cjs",
yarn_lock = "//aio:yarn.lock",
)
yarn_install(
name = "aio_example_deps",
# Rename the default js_library target from "node_modules" as this obscures the
# the source directory stamped as a filegroup in the manual BUILD contents below.
all_node_modules_target_name = "node_modules_all",
data = [
YARN_LABEL,
"//:.yarnrc",
],
# Disabled because, when False, yarn_install preserves the node_modules folder
# with bin symlinks in the external repository. This is needed to link the shared
# set of deps for example e2es.
exports_directories_only = False,
manual_build_file_contents = """\
filegroup(
name = "node_modules_files",
srcs = ["node_modules"],
)
""",
package_json = "//aio/tools/examples/shared:package.json",
yarn = YARN_LABEL,
yarn_lock = "//aio/tools/examples/shared:yarn.lock",
)
load("@aspect_bazel_lib//lib:repositories.bzl", "aspect_bazel_lib_dependencies")
aspect_bazel_lib_dependencies()
@@ -199,10 +158,10 @@ cldr_xml_data_repository(
# sass rules
http_archive(
name = "io_bazel_rules_sass",
sha256 = "4bb36a654166e1b629717e55a9c0cc30590f6615c122c054f38409c20ee5ec6f",
strip_prefix = "rules_sass-f89fc59345a5074cf184ec215e9858152b7258e7",
sha256 = "85b34f2e5e227aa586b6e0fd6942262e17b3cface596891eeef5a29a0b7467e3",
strip_prefix = "rules_sass-697e5a08bdd39aee49f2c6f4eb621ef59e677e49",
urls = [
"https://github.com/bazelbuild/rules_sass/archive/f89fc59345a5074cf184ec215e9858152b7258e7.zip",
"https://github.com/bazelbuild/rules_sass/archive/697e5a08bdd39aee49f2c6f4eb621ef59e677e49.zip",
],
)
@@ -210,30 +169,5 @@ http_archive(
load("@io_bazel_rules_sass//sass:sass_repositories.bzl", "sass_repositories")
sass_repositories(
yarn_script = YARN_LABEL,
)
# Register git toolchains
register_toolchains(
"@npm//@angular/build-tooling/bazel/git-toolchain:git_linux_toolchain",
"@npm//@angular/build-tooling/bazel/git-toolchain:git_macos_x86_toolchain",
"@npm//@angular/build-tooling/bazel/git-toolchain:git_macos_arm64_toolchain",
"@npm//@angular/build-tooling/bazel/git-toolchain:git_windows_toolchain",
)
# Fetch sauce connect (tool to open Saucelabs tunnel for Saucelabs browser tests)
http_archive(
name = "sauce_connect_linux_amd64",
build_file_content = """exports_files(["bin/sc"], visibility = ["//visibility:public"])""",
sha256 = "26b9c3630f441b47854b6032f7eca6f1d88d3f62e50ee44c27015d71a5155c36",
strip_prefix = "sc-4.8.2-linux",
url = "https://saucelabs.com/downloads/sc-4.8.2-linux.tar.gz",
)
http_archive(
name = "sauce_connect_mac",
build_file_content = """exports_files(["bin/sc"], visibility = ["//visibility:public"])""",
sha256 = "28277ce81ef9ab84f5b87b526258920a8ead44789a5034346e872629bbf38089",
strip_prefix = "sc-4.8.2-osx",
url = "https://saucelabs.com/downloads/sc-4.8.2-osx.zip",
yarn_script = "//:.yarn/releases/yarn-1.22.17.cjs",
)
+16
View File
@@ -0,0 +1,16 @@
# This file is used by the build system to adjust CSS and JS output to support the specified browsers below.
# For additional information regarding the format and rule options, please see:
# https://github.com/browserslist/browserslist#queries
# For the full list of supported browsers by the Angular framework, please see:
# https://angular.io/guide/browser-support
# You can see what browsers were selected by your queries by running:
# npx browserslist
last 1 Chrome version
last 1 Firefox version
last 2 Edge major versions
last 2 Safari major versions
last 2 iOS major versions
Firefox ESR
+28 -181
View File
@@ -13,213 +13,60 @@
"createDefaultProgram": true
},
"extends": [
"plugin:@angular-eslint/ng-cli-compat",
"plugin:@angular-eslint/ng-cli-compat--formatting-add-on",
"plugin:@angular-eslint/template/process-inline-templates"
],
"plugins": [
"@typescript-eslint",
"@angular-eslint",
"eslint-plugin-jsdoc",
"eslint-plugin-import",
"eslint-plugin-jsdoc",
"eslint-plugin-prefer-arrow"
],
"rules": {
"@angular-eslint/component-class-suffix": "error",
"@typescript-eslint/ban-types": "error",
"@angular-eslint/component-selector": [
"error",
{
"type": "element",
"prefix": "aio",
"style": "kebab-case",
"type": "element"
"style": "kebab-case"
}
],
"@angular-eslint/contextual-lifecycle": "error",
"@angular-eslint/directive-class-suffix": "error",
"@angular-eslint/directive-selector": [
"error",
{
"type": "attribute",
"prefix": "aio",
"style": "camelCase",
"type": "attribute"
"style": "camelCase"
}
],
"@angular-eslint/no-conflicting-lifecycle": "error",
"@angular-eslint/no-host-metadata-property": "off",
"@angular-eslint/no-input-rename": "error",
"@angular-eslint/no-inputs-metadata-property": "error",
"@angular-eslint/no-output-native": "error",
"@angular-eslint/no-output-on-prefix": "error",
"@angular-eslint/no-output-rename": "error",
"@angular-eslint/no-outputs-metadata-property": "error",
"@angular-eslint/use-lifecycle-interface": "error",
"@angular-eslint/use-pipe-transform-interface": "error",
"@typescript-eslint/adjacent-overload-signatures": "error",
"@typescript-eslint/array-type": "off",
"@typescript-eslint/ban-types": "error",
"@typescript-eslint/consistent-type-assertions": "error",
"@typescript-eslint/dot-notation": "error",
"@typescript-eslint/explicit-member-accessibility": "off",
"@typescript-eslint/interface-name-prefix": "off",
"@typescript-eslint/member-delimiter-style": [
"error",
{
"singleline": {
"delimiter": "comma",
"requireLast": false
}
"dot-notation": "error",
"indent": "off",
"max-len": ["error", 120],
"@typescript-eslint/member-delimiter-style": ["error", {
"singleline": {
"delimiter": "comma",
"requireLast": false
}
],
}],
"@typescript-eslint/member-ordering": "off",
"@typescript-eslint/naming-convention": "off",
"@typescript-eslint/no-empty-function": "off",
"@typescript-eslint/no-empty-interface": "error",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-inferrable-types": [
"error",
{
"ignoreParameters": true
}
],
"@typescript-eslint/no-misused-new": "error",
"@typescript-eslint/no-namespace": "error",
"@typescript-eslint/no-non-null-assertion": "error",
"@typescript-eslint/no-parameter-properties": "off",
"@typescript-eslint/no-shadow": [
"error"
],
"@typescript-eslint/no-unused-expressions": "error",
"@typescript-eslint/no-use-before-define": "off",
"@typescript-eslint/no-var-requires": "off",
"@typescript-eslint/prefer-for-of": "error",
"@typescript-eslint/prefer-function-type": "error",
"@typescript-eslint/prefer-namespace-keyword": "error",
"@typescript-eslint/quotes": [
"error",
"single",
{
"avoidEscape": true
}
],
"@typescript-eslint/semi": [
"error",
"always"
],
"@typescript-eslint/triple-slash-reference": [
"error",
{
"lib": "always",
"path": "always",
"types": "prefer-import"
}
],
"@typescript-eslint/type-annotation-spacing": "error",
"@typescript-eslint/unified-signatures": "error",
"arrow-body-style": "error",
"arrow-parens": "off",
"comma-dangle": "off",
"complexity": "off",
"constructor-super": "error",
"curly": "error",
"dot-notation": "error",
"eol-last": "error",
"eqeqeq": [
"error",
"smart"
],
"guard-for-in": "error",
"id-denylist": [
"error",
"any",
"Number",
"number",
"String",
"string",
"Boolean",
"boolean",
"Undefined",
"undefined"
],
"id-match": "error",
"import/no-deprecated": "warn",
"indent": "off",
"jsdoc/check-alignment": "error",
"jsdoc/no-types": "error",
"max-classes-per-file": "off",
"max-len": [
"error",
120
],
"new-parens": "error",
"no-bitwise": "error",
"no-caller": "error",
"no-cond-assign": "error",
"no-console": [
"error",
{
"allow": [
"log",
"warn",
"error"
]
}
],
"no-debugger": "error",
"no-empty": "off",
"no-console": ["error", {"allow": ["log", "warn", "error"]}],
"no-empty-function": "off",
"no-eval": "error",
"no-fallthrough": "error",
"no-invalid-this": "off",
"no-multiple-empty-lines": "off",
"no-new-wrappers": "error",
"no-restricted-imports": [
"error",
{
"message": "Please import directly from 'rxjs' instead",
"name": "rxjs/Rx"
}
],
"@angular-eslint/no-host-metadata-property": "off",
"no-restricted-syntax": [
"error",
{
"message": "Don't keep jasmine focus methods.",
"selector": "CallExpression[callee.name=/^(fdescribe|fit)$/]"
"selector": "CallExpression[callee.name=/^(fdescribe|fit)$/]",
"message": "Don't keep jasmine focus methods."
}
],
"no-shadow": "off",
"@typescript-eslint/no-shadow": ["error"],
"no-tabs": "error",
"no-throw-literal": "error",
"no-trailing-spaces": "error",
"no-undef-init": "error",
"no-underscore-dangle": "off",
"no-unsafe-finally": "error",
"no-unused-expressions": "off",
"no-unused-labels": "error",
"@typescript-eslint/no-unused-expressions": ["error"],
"no-use-before-define": "off",
"no-var": "error",
"object-shorthand": "error",
"one-var": [
"error",
"never"
],
"prefer-arrow/prefer-arrow-functions": "off",
"prefer-const": "error",
"quote-props": [
"error",
"as-needed"
],
"quotes": "off",
"radix": "error",
"sort-keys": "off",
"space-before-function-paren": [
"error",
{
"anonymous": "never",
"asyncArrow": "always",
"named": "never"
}
],
"use-isnan": "error",
"valid-typeof": "off"
"@typescript-eslint/quotes": ["error", "single", {"avoidEscape": true}],
"semi": "error"
}
},
{
@@ -230,11 +77,11 @@
"plugin:@angular-eslint/template/recommended"
],
"rules": {
"@angular-eslint/template/alt-text": "error",
"@angular-eslint/template/elements-content": "error",
"@angular-eslint/template/label-has-associated-control": "error",
"@angular-eslint/template/table-scope": "error",
"@angular-eslint/template/valid-aria": "error",
"@angular-eslint/template/accessibility-alt-text": "error",
"@angular-eslint/template/accessibility-elements-content": "error",
"@angular-eslint/template/accessibility-label-has-associated-control": "error",
"@angular-eslint/template/accessibility-table-scope": "error",
"@angular-eslint/template/accessibility-valid-aria": "error",
"@angular-eslint/template/click-events-have-key-events": "error",
"@angular-eslint/template/eqeqeq": "off",
"@angular-eslint/template/mouse-events-have-key-events": "error",
+1 -4
View File
@@ -5,10 +5,10 @@
/tmp
/out-tsc
/bazel-out
/src/generated
# Node
/node_modules
/tools/examples/shared/node_modules
npm-debug.log
yarn-error.log
@@ -43,9 +43,6 @@ testem.log
# e2e
protractor-results*.txt
# Example playground
content/example-playground
# System files
.DS_Store
Thumbs.db
+31 -306
View File
@@ -1,136 +1,39 @@
load("@aio_npm//@angular-devkit/architect-cli:index.bzl", "architect", "architect_test")
load("@build_bazel_rules_nodejs//:index.bzl", "copy_to_bin", "npm_package_bin")
load("//tools:defaults.bzl", "nodejs_binary")
load("@aspect_bazel_lib//lib:copy_to_directory.bzl", "copy_to_directory")
load(":local_packages_util.bzl", "link_local_packages", "substitute_local_package_deps")
load("@bazel_skylib//rules:common_settings.bzl", "bool_flag", "string_flag")
load("//aio/scripts:local_server_test.bzl", "local_server_test")
load("@aio_npm//@angular/build-tooling/bazel/remote-execution:index.bzl", "ENABLE_NETWORK")
load(":aio_targets.bzl", "aio_test")
load("@bazel_skylib//lib:collections.bzl", "collections")
load("//tools:defaults.bzl", "npm_package_bin")
# The write_source_files macro is used to write bazel outputs to the source tree and test that they are up to date.
# See: https://docs.aspect.build/aspect-build/bazel-lib/v0.5.0/docs/docs/write_source_files-docgen.html
load("@aspect_bazel_lib//lib:write_source_files.bzl", generated_files_test = "write_source_files")
exports_files([
"firebase.json",
"ngsw-config.json",
"ngsw-config.template.json",
"tsconfig.json",
])
# If set will use first party angular deps for aio targets instead of their npm equivalent
bool_flag(
name = "flag_aio_local_deps",
build_setting_default = False,
)
config_setting(
name = "aio_local_deps",
flag_values = {
":flag_aio_local_deps": "true",
},
)
# Config setting controlling the AIO architect configuration to build with
string_flag(
name = "flag_aio_build_config",
build_setting_default = "stable",
values = [
"stable",
"rc",
"next",
"archive",
],
)
config_setting(
name = "aio_build_config_stable",
flag_values = {
":flag_aio_build_config": "stable",
},
)
config_setting(
name = "aio_build_config_rc",
flag_values = {
":flag_aio_build_config": "rc",
},
)
config_setting(
name = "aio_build_config_next",
flag_values = {
":flag_aio_build_config": "next",
},
)
config_setting(
name = "aio_build_config_archive",
flag_values = {
":flag_aio_build_config": "archive",
},
)
# Run dgeni generation
# Generate ngsw-config
npm_package_bin(
name = "dgeni",
# angular.io-package is a js_library target, which copies sources to $(BINDIR)
args = ["./$(BINDIR)/aio/tools/transforms/angular.io-package"],
data = [
"//aio/tools/transforms/angular.io-package",
# Dgeni typescript package crawls type definitions for Angular dependencies
"@npm//rxjs",
"@npm//@types/node",
],
env = {
"BAZEL_DGENI_OUTPUT_PATH": "$(@D)",
"GIT_BIN": "$(GIT_BIN_PATH)",
},
exec_properties = ENABLE_NETWORK,
output_dir = True,
stamp = "@rules_nodejs//nodejs/stamp:use_stamp_flag",
# Makes remote git calls to get previous versions
tags = [
"requires-network",
],
tool = "@aio_npm//dgeni/bin:dgeni",
toolchains = [
"@npm//@angular/build-tooling/bazel/git-toolchain:current_git_toolchain",
],
visibility = ["//aio/tests/deployment:__subpackages__"],
name = "ngsw-config",
outs = ["ngsw-config_generated.json"],
args = ["$@"],
tool = "//aio/scripts:build-ngsw-config",
)
# Copy stackblitz live examples to expected assets location
copy_to_directory(
name = "stackblitz",
srcs = ["//aio/content/examples:all-stackblitz"],
replace_prefixes = {
"examples": "generated/live-examples",
# Write ngsw-config to the source directory and test that it's up to date
generated_files_test(
name = "write-ngsw-config",
files = {
"ngsw-config.json": ":ngsw-config",
},
root_paths = [
"aio/content",
],
)
# Copy example zips to expected assets location
copy_to_directory(
name = "example-zips",
srcs = ["//aio/content/examples:all-example-zips"],
replace_prefixes = {
"examples": "generated/zips",
},
root_paths = [
"aio/content",
],
)
# All source and configuration files required to build the docs app
APPLICATION_FILES = [
"angular.json",
"ngsw-config.json",
"package.json",
"tsconfig.app.json",
"tsconfig.json",
"tsconfig.worker.json",
"security-exemptions.json",
"//aio/src/assets",
"//aio/src/assets/js",
] + glob(
["src/**/*"],
exclude = [
@@ -142,20 +45,14 @@ APPLICATION_FILES = [
],
)
# Dependencies required to build the docs app.
# External dependencies from aio/package.json required to build the docs app.
APPLICATION_DEPS = [
":dgeni",
":stackblitz",
":example-zips",
"//aio/src/generated:ngsw-config",
"@aio_npm//@angular-devkit/build-angular",
"@aio_npm//@angular-eslint/builder",
"@aio_npm//@angular/animations",
"@aio_npm//@angular/cdk",
"@aio_npm//@angular/cli",
"@aio_npm//@angular/common",
"@aio_npm//@angular/compiler",
"@aio_npm//@angular/compiler-cli",
"@aio_npm//@angular/core",
"@aio_npm//@angular/elements",
"@aio_npm//@angular/forms",
@@ -181,9 +78,8 @@ TEST_FILES = APPLICATION_FILES + [
["src/**/*.spec.ts"],
)
# Dependencies required to test the docs app
# External dependencies from aio/package.json required to test the docs app
TEST_DEPS = APPLICATION_DEPS + [
"@aio_npm//@angular/build-tooling/bazel/browsers/chromium",
"@aio_npm//@types/jasmine",
"@aio_npm//@types/node",
"@aio_npm//assert",
@@ -193,200 +89,29 @@ TEST_DEPS = APPLICATION_DEPS + [
"@aio_npm//karma-coverage",
"@aio_npm//karma-jasmine",
"@aio_npm//karma-jasmine-html-reporter",
"//aio/tools:windows-chromium-path",
"@aio_npm//puppeteer",
"@aio_npm//timezone-mock",
]
# All sources, specs, and config files required to serve the app
# and run e2e tests against it
E2E_FILES = APPLICATION_FILES + glob(["tests/e2e/**"])
# Dependencies required to run the e2e tests
E2E_DEPS = APPLICATION_DEPS + [
"@aio_npm//@angular/build-tooling/bazel/browsers/chromium",
"@aio_npm//@types/jasmine",
"@aio_npm//@types/node",
"@aio_npm//jasmine-spec-reporter",
"@aio_npm//protractor",
"@aio_npm//ts-node",
"//aio/tools:windows-chromium-path",
]
# Poll period for architect rules that watch for changes
ARCHITECT_POLL_MS = 1000
# Stamp npm_link targets for all dependencies that correspond to a
# first-party equivalent pacakge in angular.
link_local_packages(all_aio_deps = collections.uniq(APPLICATION_DEPS + TEST_DEPS + E2E_DEPS))
copy_to_bin(
name = "application_files_bin",
srcs = APPLICATION_FILES,
)
architect(
name = "build-app",
args = select({
":aio_build_config_stable": ["site:build:stable"],
":aio_build_config_next": ["site:build:next"],
":aio_build_config_rc": ["site:build:rc"],
":aio_build_config_archive": ["site:build:archive"],
}) + [
"--output-path=build-app",
],
chdir = "$(RULEDIR)",
data = [":application_files_bin"] + select({
":aio_local_deps": substitute_local_package_deps(APPLICATION_DEPS),
"//conditions:default": APPLICATION_DEPS,
}),
output_dir = True,
# When building with local packages (--config=aio_local_deps), RBE complains about
# the input tree being too large (> 70,000 files).
tags = ["no-remote-exec"],
)
npm_package_bin(
name = "404-page",
outs = ["404.html"],
args = [
"$(execpath src/404-body.html)",
"$(execpath :build-app)",
"$@",
],
data = [
"src/404-body.html",
":build-app",
],
tool = "//aio/scripts:build-404-page",
)
# Prepare the final build artifact which substitutes a generated 404 page
# into the architect output.
copy_to_directory(
name = "build",
srcs = [
":404-page",
":build-app",
args = [
"site:build:stable",
"--outputPath=../$(@D)",
],
replace_prefixes = {
"build-app": "",
},
visibility = ["//aio/scripts/deploy-to-firebase:__pkg__"],
chdir = package_name(),
configuration_env_vars = ["NG_BUILD_CACHE"],
data = APPLICATION_FILES + APPLICATION_DEPS,
output_dir = True,
)
TEST_DATA = TEST_FILES + select({
":aio_local_deps": substitute_local_package_deps(TEST_DEPS),
"//conditions:default": TEST_DEPS,
})
aio_test(
architect_test(
name = "test",
args = [
"site:test",
"--no-watch",
],
data = TEST_DATA,
)
aio_test(
name = "test-and-watch",
args = [
"site:test",
"--watch",
"--poll=%d" % ARCHITECT_POLL_MS,
],
data = TEST_DATA,
tags = [
"ibazel_notify_changes",
"manual",
],
)
architect_test(
name = "e2e",
size = "large",
args = [
"site:e2e",
"--no-webdriver-update",
"--port=0",
],
chdir = package_name(),
data = E2E_FILES + select({
":aio_local_deps": substitute_local_package_deps(E2E_DEPS),
"//conditions:default": E2E_DEPS,
}),
env = {
"CHROME_BIN": "../$(CHROMIUM)",
"CHROMEDRIVER_BIN": "../$(CHROMEDRIVER)",
},
# TODO: investigate why those tests became more flaky
# (presumably after AIO migration to Bazel)
flaky = True,
toolchains = [
"@aio_npm//@angular/build-tooling/bazel/browsers/chromium:toolchain_alias",
],
)
# Build and serve the app. Run with ibazel to watch for changes and re-serve.
architect(
name = "serve",
args = [
"site:serve",
"--open",
"--poll=%d" % ARCHITECT_POLL_MS,
"--live-reload",
"--watch",
],
chdir = package_name(),
data = APPLICATION_FILES + select({
":aio_local_deps": substitute_local_package_deps(APPLICATION_DEPS),
"//conditions:default": APPLICATION_DEPS,
}),
tags = ["ibazel_notify_changes"],
)
# Build and serve the app, watch for changes, and run a fast but low-fidelity
# rebuild when docs change. Watching and serving is a part of the node script,
# so there is no need to run with ibazel, which would be slow as it would redo
# the full dgeni build on each change.
nodejs_binary(
name = "docs-watch",
chdir = package_name(),
data = APPLICATION_FILES + [
"//aio/scripts:fast-serve-and-watch",
] + select({
":aio_local_deps": substitute_local_package_deps(APPLICATION_DEPS),
"//conditions:default": APPLICATION_DEPS,
}),
enable_linker = True,
entry_point = "//aio/scripts:fast-serve-and-watch.js",
env = {
# Have the authors package output its low-fi dgeni build
# to a different directory in the runfiles tree because bazel
# write-protects original dgeni runfiles. Then in angular.json
# we list this folder last under assets so that it will
# take precedence over the original dgeni assets, effectively
# overwriting select sources and then serving them.
"BAZEL_DGENI_OUTPUT_PATH": "dgeni-fast",
},
)
local_server_test(
name = "test-a11y-score-localhost",
args = ["LOCALHOST_URL"],
entry_point = "//aio/scripts:test-aio-a11y",
serve_target = ":build",
)
local_server_test(
name = "test-pwa-score-localhost",
args = [
"LOCALHOST_URL",
"all:0,pwa:100",
],
data = [
"//aio/scripts:audit-web-app",
],
entry_point = "//aio/scripts:audit-web-app-script",
serve_target = ":build",
configuration_env_vars = ["NG_BUILD_CACHE"],
data = TEST_FILES + TEST_DEPS,
)
+48 -25
View File
@@ -9,57 +9,67 @@ Everything in this folder is part of the documentation project. This includes:
<a name="developer-tasks"></a>
## Developer tasks
We use [Yarn](https://yarnpkg.com) to manage the dependencies and development tasks. Behind the scenes, [Bazel](https://bazel.build/) is used to build targets and run tests.
We use [Yarn](https://yarnpkg.com) to manage the dependencies and to run build tasks.
You should run all these tasks from the `angular/aio` folder.
Here are the most important tasks you might need to use:
* `yarn` - install all the dependencies.
* `yarn build` - create a development build of the application.
* `yarn build-prod` - create a production build of the application.
* `yarn build-local` - same as `build`, but uses locally built Angular packages from source code rather than from npm.
* `yarn start` - run a development web server that watches, rebuilds, and reloads the page when there are changes to the source code or docs.
* `yarn start-local` - same as `start`, but uses local Angular packages.
* `yarn test` - run all the unit tests for the doc-viewer once.
* `yarn test-local` - similar to `test`, but tests against locally built Angular packages.
* `yarn test-and-watch` - watch all the source files for the doc-viewer, and run all the unit tests when any change.
* `yarn e2e` - run all the e2e tests for the doc-viewer.
* `yarn e2e-local` - similar to `e2e`, but tests against locally built Angular packages.
* `yarn setup` - install all the dependencies, boilerplate, stackblitz, zips and run dgeni on the docs.
* `yarn setup-local` - same as `setup`, but build the Angular packages from the source code and use these locally built versions (instead of the ones fetched from npm) for aio and docs examples boilerplate.
* `yarn build` - create a production build of the application (after installing dependencies, boilerplate, etc).
* `yarn build-local` - same as `build`, but use `setup-local` instead of `setup`.
* `yarn start` - run a development web server that watches the files; then builds the doc-viewer and reloads the page, as necessary.
* `yarn serve-and-sync` - run both the `docs-watch` and `start` in the same console.
* `yarn lint` - check that the doc-viewer code follows our style rules.
* `yarn test` - watch all the source files, for the doc-viewer, and run all the unit tests when any change.
* `yarn test --watch=false` - run all the unit tests once.
* `yarn e2e` - run all the e2e tests for the doc-viewer.
* `yarn docs-watch` - similar to `start`, but only watches for docs changes and uses a faster, low-fidelity build ideal for quick editing.
* `yarn docs-test` - run the unit tests for the doc generation code.
* `yarn docs` - generate all the docs from the source files.
* `yarn docs-watch` - watch the Angular source and the docs files and run a short-circuited doc-gen for the docs that changed.
* `yarn docs-lint` - check that the doc gen code follows our style rules.
* `yarn docs-test` - run the unit tests for the doc generation code.
* `yarn boilerplate:add` - generate all the boilerplate code for the examples, so that they can be run locally.
* `yarn boilerplate:remove` - remove all the boilerplate code that was added via `yarn boilerplate:add`.
* `yarn create-example` - create a new example directory containing initial source files.
* `yarn example-playground <exampleName>` - set up a playground to manually test an example combined with its boilerplate files
- `--local` - link locally build Angular packages as deps
- `--watch` - update the playground when sources change
* `yarn generate-stackblitz` - generate the stackblitz files that are used by the `live-example` tags in the docs.
* `yarn generate-zips` - generate the zip files from the examples. Zip available via the `live-example` tags in the docs.
* `yarn example-e2e` - run all e2e tests for examples. Available options:
- `--local`: run e2e tests against locally built Angular packages.
- `--setup`: generate boilerplate, force webdriver update & other setup, then run tests.
- `--local`: run e2e tests with the local version of Angular contained in the "dist" folder.
_Requires `--setup` in order to take effect._
- `--filter=foo`: limit e2e tests to those containing the word "foo".
- `--exclude=bar`: exclude e2e tests containing the word "bar".
> **Note for Windows users**
>
> The underlying Bazel build requires creating [symbolic links](https://en.wikipedia.org/wiki/Symbolic_link) (see [here](./tools/examples/README.md#symlinked-node_modules) for details). On Windows, this requires to either have [Developer Mode enabled](https://blogs.windows.com/windowsdeveloper/2016/12/02/symlinks-windows-10) (supported on Windows 10 or newer) or run the setup commands as administrator.
> Setting up the examples involves creating some [symbolic links](https://en.wikipedia.org/wiki/Symbolic_link) (see [here](./tools/examples/README.md#symlinked-node_modules) for details). On Windows, this requires to either have [Developer Mode enabled](https://blogs.windows.com/windowsdeveloper/2016/12/02/symlinks-windows-10) (supported on Windows 10 or newer) or run the setup commands as administrator.
>
> The affected commands are:
> - `yarn setup` / `yarn setup-*`
> - `yarn build` / `yarn build-*`
> - `yarn boilerplate:add`
> - `yarn example-e2e --setup`
## Using ServiceWorker locally
Running `yarn start` (even when explicitly targeting production mode) does not set up the
ServiceWorker. If you want to test the ServiceWorker locally, you can use `yarn build` and then
serve the files with `yarn http-server ../dist/bin/aio/build -p 4200`.
serve the files in `dist/` with `yarn http-server dist -p 4200`.
## Guide to authoring
There are two types of content in the documentation:
* **API docs**: descriptions of all that make up the Angular platform, such as the modules, classes, interfaces or decorators.
* **API docs**: descriptions of the modules, classes, interfaces, decorators, etc that make up the Angular platform.
API docs are generated directly from the source code.
The source code is contained in TypeScript files, located in the `angular/packages` folder.
Each API item may have a preceding comment, which contains JSDoc style tags and content.
The content is written in markdown. To generate docs, each package's files need to be explicitly included in the [packages/BUILD.bazel](../packages/BUILD.bazel) file under the `files_for_docgen` target.
The content is written in markdown.
* **Other content**: guides, tutorials, and other marketing material.
All other content is written using markdown in text files, located in the `angular/aio/content` folder.
@@ -77,7 +87,7 @@ writing guide pages, explains how to use the documentation classes and component
### Generating the complete docs
Running the `yarn build` or `yarn start` tasks will automatically generate the docs. This will process all the source files (API and other),
The main task for generating the docs is `yarn docs`. This will process all the source files (API and other),
extracting the documentation and generating JSON files that can be consumed by the doc-viewer.
### Partial doc generation for editors
@@ -99,13 +109,26 @@ correctly.
The general setup is as follows:
* Open a terminal, ensure the dependencies are installed, then start the doc-viewer:
* Open a terminal, ensure the dependencies are installed; run an initial doc generation; then start the doc-viewer:
```bash
yarn setup
yarn start
```
* Open a second terminal and start watching the docs
```bash
yarn docs-watch
```
* A browser will open automatically at https://localhost:4200/. Navigate to the document on which you want to work.
>Alternatively, try the consolidated `serve-and-sync` command that builds, watches and serves in the same terminal window
```bash
yarn serve-and-sync
```
* Open a browser at https://localhost:4200/ and navigate to the document on which you want to work.
You can automatically open the browser by using `yarn start -o` in the first terminal.
* Make changes to the page's associated doc or example files. Every time a file is saved, the doc will
be regenerated, the app will rebuild and the page will reload.
-42
View File
@@ -1,42 +0,0 @@
# Update the angular.io app
The dependencies of the angular.io app (including Angular, Angular Material and Angular CLI) are automatically updated using [Renovate](https://renovatebot.com/).
However, it is useful to periodically also manually update the app to more closely match (in file layout, configs, etc.) what a new Angular CLI app would look like.
This is typically only needed once for each new major Angular version.
Since angular.io is an Angular CLI app, we can take advantage of `ng update` to apply migrations.
Follow these steps to align the angular.io app with new CLI apps.
> **Note:**
> The following steps assume that the related Angular dependencies have already been updated in [aio/package.json](./package.json) (for example, automatically by Renovate).
> **Note:**
> All commands shown below are expected to be executed from inside the [aio/](./) directory (unless specified otherwise).
- Determine (for example, by examining git history) what is the last versions for which this process was performed.
These will be referred to as `<FROM_VERSION_*>`.
If you can't determine these, use arbitrary versions, such as the previous major version.
- Run the following commands to automatically apply any available migrations to the project:
```sh
# Ensure dependencies are installed.
yarn install
# Migrate project to new versions.
yarn ng update @angular/cli --allow-dirty --migrate-only --from=<FROM_VERSION_CLI>
yarn ng update @angular/core --allow-dirty --migrate-only --from=<FROM_VERSION_ANGULAR>
yarn ng update @angular/material --allow-dirty --migrate-only --from=<FROM_VERSION_MATERIAL>
```
> **Note:**
> Depending on the number of changes generated from each `ng update` command, it might make sense to create a separate commit for each update.
- Inspect [package.json](./package.json) to determine what is the current version of Angular CLI (i.e. `@angular/cli`) used in the app.
This will be referred to as `<TO_VERSION_CLI>`.
- Use the [angular-cli-diff](https://github.com/cexbrayat/angular-cli-diff) repository to discover more changes (which are not automatically applied via `ng update` migrations) between Angular CLI apps of different versions.
Visit https://github.com/cexbrayat/angular-cli-diff/compare/<FROM_VERSION_CLI>...<TO_VERSION_CLI>, inspect the changes between the two versions and apply the ones that make sense to the angular.io source code.
- Commit all changes and [submit a pull request](../CONTRIBUTING.md#submit-pr).
@@ -0,0 +1,3 @@
scripts-js/lib
scripts-js/node_modules
scripts-js/**/test
+179
View File
@@ -0,0 +1,179 @@
# Image metadata and config
FROM debian:bullseye
LABEL name="angular.io PR preview" \
description="This image implements the PR preview functionality for angular.io." \
vendor="Angular" \
version="1.0"
VOLUME /aio-secrets
VOLUME /var/www/aio-builds
VOLUME /dockerbuild
EXPOSE 80 443
# Build-time args and env vars
# The AIO_ARTIFACT_PATH path needs to be kept in synch with the value of
# `aio_preview->steps->store_artifacts->destination` property in `.circleci/config.yml`
ARG AIO_ARTIFACT_PATH=aio/dist/aio-snapshot.tgz
ARG TEST_AIO_ARTIFACT_PATH=$AIO_ARTIFACT_PATH
ARG AIO_BUILDS_DIR=/var/www/aio-builds
ARG TEST_AIO_BUILDS_DIR=/tmp/aio-builds
ARG AIO_DOMAIN_NAME=ngbuilds.io
ARG TEST_AIO_DOMAIN_NAME=$AIO_DOMAIN_NAME.localhost
ARG AIO_GITHUB_ORGANIZATION=angular
ARG TEST_AIO_GITHUB_ORGANIZATION=test-org
ARG AIO_GITHUB_REPO=angular
ARG TEST_AIO_GITHUB_REPO=test-repo
ARG AIO_GITHUB_TEAM_SLUGS=aio-auto-previews,aio-contributors
ARG TEST_AIO_GITHUB_TEAM_SLUGS=test-team-1,test-team-2
ARG AIO_NGINX_HOSTNAME=$AIO_DOMAIN_NAME
ARG TEST_AIO_NGINX_HOSTNAME=$TEST_AIO_DOMAIN_NAME
ARG AIO_NGINX_PORT_HTTP=80
ARG TEST_AIO_NGINX_PORT_HTTP=8080
ARG AIO_NGINX_PORT_HTTPS=443
ARG TEST_AIO_NGINX_PORT_HTTPS=4433
ARG AIO_SIGNIFICANT_FILES_PATTERN='^(?:aio|packages)/(?!.*[._]spec\\.[jt]s$)'
ARG TEST_AIO_SIGNIFICANT_FILES_PATTERN=$AIO_SIGNIFICANT_FILES_PATTERN
ARG AIO_TRUSTED_PR_LABEL="aio: preview"
ARG TEST_AIO_TRUSTED_PR_LABEL=$AIO_TRUSTED_PR_LABEL
ARG AIO_PREVIEW_SERVER_HOSTNAME=preview.localhost
ARG TEST_AIO_PREVIEW_SERVER_HOSTNAME=$AIO_PREVIEW_SERVER_HOSTNAME
ARG AIO_ARTIFACT_MAX_SIZE=26214400
ARG TEST_AIO_ARTIFACT_MAX_SIZE=200
ARG AIO_PREVIEW_SERVER_PORT=3000
ARG TEST_AIO_PREVIEW_SERVER_PORT=3001
ENV AIO_ARTIFACT_PATH=$AIO_ARTIFACT_PATH TEST_AIO_ARTIFACT_PATH=$TEST_AIO_ARTIFACT_PATH \
AIO_BUILDS_DIR=$AIO_BUILDS_DIR TEST_AIO_BUILDS_DIR=$TEST_AIO_BUILDS_DIR \
AIO_DOMAIN_NAME=$AIO_DOMAIN_NAME TEST_AIO_DOMAIN_NAME=$TEST_AIO_DOMAIN_NAME \
AIO_GITHUB_ORGANIZATION=$AIO_GITHUB_ORGANIZATION TEST_AIO_GITHUB_ORGANIZATION=$TEST_AIO_GITHUB_ORGANIZATION \
AIO_GITHUB_REPO=$AIO_GITHUB_REPO TEST_AIO_GITHUB_REPO=$TEST_AIO_GITHUB_REPO \
AIO_GITHUB_TEAM_SLUGS=$AIO_GITHUB_TEAM_SLUGS TEST_AIO_GITHUB_TEAM_SLUGS=$TEST_AIO_GITHUB_TEAM_SLUGS \
AIO_LOCALCERTS_DIR=/etc/ssl/localcerts TEST_AIO_LOCALCERTS_DIR=/etc/ssl/localcerts-test \
AIO_NGINX_HOSTNAME=$AIO_NGINX_HOSTNAME TEST_AIO_NGINX_HOSTNAME=$TEST_AIO_NGINX_HOSTNAME \
AIO_NGINX_LOGS_DIR=/var/log/aio/nginx TEST_AIO_NGINX_LOGS_DIR=/var/log/aio/nginx-test \
AIO_NGINX_PORT_HTTP=$AIO_NGINX_PORT_HTTP TEST_AIO_NGINX_PORT_HTTP=$TEST_AIO_NGINX_PORT_HTTP \
AIO_NGINX_PORT_HTTPS=$AIO_NGINX_PORT_HTTPS TEST_AIO_NGINX_PORT_HTTPS=$TEST_AIO_NGINX_PORT_HTTPS \
AIO_SCRIPTS_JS_DIR=/usr/share/aio-scripts-js \
AIO_SCRIPTS_SH_DIR=/usr/share/aio-scripts-sh \
AIO_SIGNIFICANT_FILES_PATTERN=$AIO_SIGNIFICANT_FILES_PATTERN TEST_AIO_SIGNIFICANT_FILES_PATTERN=$TEST_AIO_SIGNIFICANT_FILES_PATTERN \
AIO_TRUSTED_PR_LABEL=$AIO_TRUSTED_PR_LABEL TEST_AIO_TRUSTED_PR_LABEL=$TEST_AIO_TRUSTED_PR_LABEL \
AIO_PREVIEW_SERVER_HOSTNAME=$AIO_PREVIEW_SERVER_HOSTNAME TEST_AIO_PREVIEW_SERVER_HOSTNAME=$TEST_AIO_PREVIEW_SERVER_HOSTNAME \
AIO_ARTIFACT_MAX_SIZE=$AIO_ARTIFACT_MAX_SIZE TEST_AIO_ARTIFACT_MAX_SIZE=$TEST_AIO_ARTIFACT_MAX_SIZE \
AIO_PREVIEW_SERVER_PORT=$AIO_PREVIEW_SERVER_PORT TEST_AIO_PREVIEW_SERVER_PORT=$TEST_AIO_PREVIEW_SERVER_PORT \
AIO_WWW_USER=www-data \
NODE_ENV=production
# Create directory for logs
RUN mkdir /var/log/aio
# Add extra package sources
RUN apt-get update -y && apt-get install -y curl
RUN curl --silent --show-error --location https://deb.nodesource.com/setup_14.x | bash -
RUN curl --silent --show-error https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add -
RUN echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list
# Install packages
# NOTE: Many packages make older versions unavailable on the repositories, so we cannot pin to
# specific versions :(
# See, for example:
# - https://github.com/nodesource/distributions/issues/33
# - https://askubuntu.com/questions/715104/how-can-i-downgrade-openssl-via-apt-get
RUN apt-get update -y && apt-get install -y \
cron \
dnsmasq \
nano \
nginx \
nodejs \
openssl \
rsyslog \
vim \
yarn=1.22.18-1
RUN yarn global add pm2@5.2.0
# Set up log rotation
COPY logrotate/* /etc/logrotate.d/
RUN chmod 0644 /etc/logrotate.d/*
# Set up cronjobs
COPY cronjobs/aio-builds-cleanup /etc/cron.d/
RUN chmod 0744 /etc/cron.d/aio-builds-cleanup
RUN crontab /etc/cron.d/aio-builds-cleanup
RUN printenv | grep AIO_ >> /etc/environment
# Set up dnsmasq
COPY dnsmasq/dnsmasq.conf /etc/
RUN sed -i "s|{{\$AIO_NGINX_HOSTNAME}}|$AIO_NGINX_HOSTNAME|g" /etc/dnsmasq.conf
RUN sed -i "s|{{\$AIO_PREVIEW_SERVER_HOSTNAME}}|$AIO_PREVIEW_SERVER_HOSTNAME|g" /etc/dnsmasq.conf
RUN sed -i "s|{{\$TEST_AIO_NGINX_HOSTNAME}}|$TEST_AIO_NGINX_HOSTNAME|g" /etc/dnsmasq.conf
RUN sed -i "s|{{\$TEST_AIO_PREVIEW_SERVER_HOSTNAME}}|$TEST_AIO_PREVIEW_SERVER_HOSTNAME|g" /etc/dnsmasq.conf
# Set up SSL/TLS certificates
COPY nginx/create-selfsigned-cert.sh /tmp/
RUN chmod a+x /tmp/create-selfsigned-cert.sh
RUN /tmp/create-selfsigned-cert.sh "selfcert-prod" "$AIO_NGINX_HOSTNAME" "$AIO_LOCALCERTS_DIR"
RUN /tmp/create-selfsigned-cert.sh "selfcert-test" "$TEST_AIO_NGINX_HOSTNAME" "$TEST_AIO_LOCALCERTS_DIR"
RUN rm /tmp/create-selfsigned-cert.sh
RUN update-ca-certificates
# Set up nginx (for production and testing)
RUN sed -i -E "s|^user\s+\S+;|user $AIO_WWW_USER;|" /etc/nginx/nginx.conf
RUN rm -f /etc/nginx/conf.d/*
RUN rm -f /etc/nginx/sites-enabled/*
COPY nginx/aio-builds.conf /etc/nginx/conf.d/aio-builds-prod.conf
RUN sed -i "s|{{\$AIO_BUILDS_DIR}}|$AIO_BUILDS_DIR|g" /etc/nginx/conf.d/aio-builds-prod.conf
RUN sed -i "s|{{\$AIO_DOMAIN_NAME}}|$AIO_DOMAIN_NAME|g" /etc/nginx/conf.d/aio-builds-prod.conf
RUN sed -i "s|{{\$AIO_LOCALCERTS_DIR}}|$AIO_LOCALCERTS_DIR|g" /etc/nginx/conf.d/aio-builds-prod.conf
RUN sed -i "s|{{\$AIO_NGINX_LOGS_DIR}}|$AIO_NGINX_LOGS_DIR|g" /etc/nginx/conf.d/aio-builds-prod.conf
RUN sed -i "s|{{\$AIO_NGINX_PORT_HTTP}}|$AIO_NGINX_PORT_HTTP|g" /etc/nginx/conf.d/aio-builds-prod.conf
RUN sed -i "s|{{\$AIO_NGINX_PORT_HTTPS}}|$AIO_NGINX_PORT_HTTPS|g" /etc/nginx/conf.d/aio-builds-prod.conf
RUN sed -i "s|{{\$AIO_PREVIEW_SERVER_HOSTNAME}}|$AIO_PREVIEW_SERVER_HOSTNAME|g" /etc/nginx/conf.d/aio-builds-prod.conf
RUN sed -i "s|{{\$AIO_ARTIFACT_MAX_SIZE}}|$AIO_ARTIFACT_MAX_SIZE|g" /etc/nginx/conf.d/aio-builds-prod.conf
RUN sed -i "s|{{\$AIO_PREVIEW_SERVER_PORT}}|$AIO_PREVIEW_SERVER_PORT|g" /etc/nginx/conf.d/aio-builds-prod.conf
COPY nginx/aio-builds.conf /etc/nginx/conf.d/aio-builds-test.conf
RUN sed -i "s|{{\$AIO_BUILDS_DIR}}|$TEST_AIO_BUILDS_DIR|g" /etc/nginx/conf.d/aio-builds-test.conf
RUN sed -i "s|{{\$AIO_DOMAIN_NAME}}|$TEST_AIO_DOMAIN_NAME|g" /etc/nginx/conf.d/aio-builds-test.conf
RUN sed -i "s|{{\$AIO_LOCALCERTS_DIR}}|$TEST_AIO_LOCALCERTS_DIR|g" /etc/nginx/conf.d/aio-builds-test.conf
RUN sed -i "s|{{\$AIO_NGINX_LOGS_DIR}}|$TEST_AIO_NGINX_LOGS_DIR|g" /etc/nginx/conf.d/aio-builds-test.conf
RUN sed -i "s|{{\$AIO_NGINX_PORT_HTTP}}|$TEST_AIO_NGINX_PORT_HTTP|g" /etc/nginx/conf.d/aio-builds-test.conf
RUN sed -i "s|{{\$AIO_NGINX_PORT_HTTPS}}|$TEST_AIO_NGINX_PORT_HTTPS|g" /etc/nginx/conf.d/aio-builds-test.conf
RUN sed -i "s|{{\$AIO_PREVIEW_SERVER_HOSTNAME}}|$TEST_AIO_PREVIEW_SERVER_HOSTNAME|g" /etc/nginx/conf.d/aio-builds-test.conf
RUN sed -i "s|{{\$AIO_ARTIFACT_MAX_SIZE}}|$TEST_AIO_ARTIFACT_MAX_SIZE|g" /etc/nginx/conf.d/aio-builds-test.conf
RUN sed -i "s|{{\$AIO_PREVIEW_SERVER_PORT}}|$TEST_AIO_PREVIEW_SERVER_PORT|g" /etc/nginx/conf.d/aio-builds-test.conf
# Set up pm2
RUN pm2 startup --user root > /dev/null
# Set up the shell scripts
COPY scripts-sh/ $AIO_SCRIPTS_SH_DIR/
RUN chmod a+x $AIO_SCRIPTS_SH_DIR/*
RUN find $AIO_SCRIPTS_SH_DIR -maxdepth 1 -type f -printf "%P\n" \
| while read file; do ln -s $AIO_SCRIPTS_SH_DIR/$file /usr/local/bin/aio-${file%.*}; done
# Set up the Node.js scripts
COPY scripts-js/ $AIO_SCRIPTS_JS_DIR/
RUN yarn --cwd="$AIO_SCRIPTS_JS_DIR/" install --production --frozen-lockfile
# Set up health check
HEALTHCHECK --interval=5m CMD /usr/local/bin/aio-health-check
# Go!
WORKDIR /
CMD aio-init && tail -f /dev/null
@@ -0,0 +1,2 @@
# Periodically clean up builds that do not correspond to currently open PRs
0 12 * * * /usr/local/bin/aio-clean-up >> /var/log/cron.log 2>&1
@@ -0,0 +1,16 @@
# Do not read /etc/resolv.conf. Get servers from this file instead.
no-resolv
server=8.8.8.8
server=8.8.4.4
# Listen for DHCP and DNS requests only on this address.
listen-address=127.0.0.1
# Force an IP address for these domains.
address=/{{$AIO_NGINX_HOSTNAME}}/127.0.0.1
address=/{{$AIO_PREVIEW_SERVER_HOSTNAME}}/127.0.0.1
address=/{{$TEST_AIO_NGINX_HOSTNAME}}/127.0.0.1
address=/{{$TEST_AIO_PREVIEW_SERVER_HOSTNAME}}/127.0.0.1
# Run as root (required from inside docker container).
user=root
@@ -0,0 +1,9 @@
/var/log/aio/clean-up.log /var/log/aio/init.log /var/log/aio/verify-setup.log {
compress
create
delaycompress
missingok
monthly
notifempty
rotate 6
}
@@ -0,0 +1,13 @@
/var/log/aio/nginx/*.log /var/log/aio/nginx-test/*.log {
compress
create
delaycompress
missingok
monthly
notifempty
rotate 6
sharedscripts
postrotate
service nginx rotate >/dev/null 2>&1
endscript
}
@@ -0,0 +1,9 @@
/var/log/aio/preview-server-*.log {
compress
copytruncate
delaycompress
missingok
monthly
notifempty
rotate 6
}
@@ -0,0 +1,123 @@
# Redirect all HTTP traffic to HTTPS
server {
server_name _;
listen {{$AIO_NGINX_PORT_HTTP}} default_server;
listen [::]:{{$AIO_NGINX_PORT_HTTP}};
access_log {{$AIO_NGINX_LOGS_DIR}}/access.log;
error_log {{$AIO_NGINX_LOGS_DIR}}/error.log;
# Ideally we want 308 (permanent + keep original method),
# but it is relatively new and not supported by some clients (e.g. cURL).
return 307 https://$host:{{$AIO_NGINX_PORT_HTTPS}}$request_uri;
}
# Serve PR-preview requests
server {
server_name "~^pr(?<pr>[1-9][0-9]*)-(?<sha>[0-9a-f]{7,40})\.";
listen {{$AIO_NGINX_PORT_HTTPS}} ssl http2;
listen [::]:{{$AIO_NGINX_PORT_HTTPS}} ssl http2;
ssl_certificate {{$AIO_LOCALCERTS_DIR}}/{{$AIO_DOMAIN_NAME}}.crt;
ssl_certificate_key {{$AIO_LOCALCERTS_DIR}}/{{$AIO_DOMAIN_NAME}}.key;
ssl_prefer_server_ciphers on;
ssl_ciphers EECDH+CHACHA20:EECDH+AES128:RSA+AES128:EECDH+AES256:RSA+AES256:EECDH+3DES:RSA+3DES:!MD5;
root {{$AIO_BUILDS_DIR}}/$pr/$sha;
disable_symlinks on from=$document_root;
index index.html;
gzip on;
gzip_comp_level 7;
gzip_types *;
access_log {{$AIO_NGINX_LOGS_DIR}}/access.log;
error_log {{$AIO_NGINX_LOGS_DIR}}/error.log;
error_page 404 /404.html;
location "=/404.html" {
internal;
}
location "~/[^/]+\.[^/]+$" {
try_files $uri $uri/ =404;
}
location / {
try_files $uri $uri/ /index.html =404;
}
}
# Handle all other requests
server {
server_name _;
listen {{$AIO_NGINX_PORT_HTTPS}} ssl http2 default_server;
listen [::]:{{$AIO_NGINX_PORT_HTTPS}} ssl http2;
ssl_certificate {{$AIO_LOCALCERTS_DIR}}/{{$AIO_DOMAIN_NAME}}.crt;
ssl_certificate_key {{$AIO_LOCALCERTS_DIR}}/{{$AIO_DOMAIN_NAME}}.key;
ssl_prefer_server_ciphers on;
ssl_ciphers EECDH+CHACHA20:EECDH+AES128:RSA+AES128:EECDH+AES256:RSA+AES256:EECDH+3DES:RSA+3DES:!MD5;
access_log {{$AIO_NGINX_LOGS_DIR}}/access.log;
error_log {{$AIO_NGINX_LOGS_DIR}}/error.log;
# Health check
location "~^/health-check/?$" {
add_header Content-Type text/plain;
return 200 '';
}
# Check PRs previewability
location "~^/can-have-public-preview/\d+/?$" {
if ($request_method != "GET") {
add_header Allow "GET";
return 405;
}
proxy_pass_request_headers on;
proxy_redirect off;
proxy_method GET;
proxy_pass http://{{$AIO_PREVIEW_SERVER_HOSTNAME}}:{{$AIO_PREVIEW_SERVER_PORT}}$request_uri;
resolver 127.0.0.1;
}
# Notify about CircleCI builds
location "~^/circle-build/?$" {
if ($request_method != "POST") {
add_header Allow "POST";
return 405;
}
proxy_pass_request_headers on;
proxy_redirect off;
proxy_method POST;
proxy_pass http://{{$AIO_PREVIEW_SERVER_HOSTNAME}}:{{$AIO_PREVIEW_SERVER_PORT}}$request_uri;
resolver 127.0.0.1;
}
# Notify about PR changes
location "~^/pr-updated/?$" {
if ($request_method != "POST") {
add_header Allow "POST";
return 405;
}
proxy_pass_request_headers on;
proxy_redirect off;
proxy_method POST;
proxy_pass http://{{$AIO_PREVIEW_SERVER_HOSTNAME}}:{{$AIO_PREVIEW_SERVER_PORT}}$request_uri;
resolver 127.0.0.1;
}
# Everything else
location / {
return 404;
}
}
@@ -0,0 +1,20 @@
#!/bin/bash
set -eu -o pipefail
# Variables
confFile=/tmp/$1.conf
domainName=$2
outDir=$3
# Create certificate
cp /etc/ssl/openssl.cnf "$confFile"
echo "[subjectAltName]" >> "$confFile"
echo "subjectAltName = DNS:$domainName, DNS:*.$domainName" >> "$confFile"
mkdir -p $outDir
openssl req -days 365 -newkey rsa:2048 -nodes -sha256 -x509 \
-config "$confFile" -extensions subjectAltName -subj "/CN=$domainName" \
-out "$outDir/$domainName.crt" -keyout "$outDir/$domainName.key"
chmod -R 400 "$outDir"
cp "$outDir/$domainName.crt" /usr/local/share/ca-certificates
@@ -0,0 +1,2 @@
/dist
/node_modules
@@ -0,0 +1,128 @@
// Imports
import * as fs from 'fs';
import * as path from 'path';
import * as shell from 'shelljs';
import {HIDDEN_DIR_PREFIX} from '../common/constants';
import {GithubApi} from '../common/github-api';
import {GithubPullRequests} from '../common/github-pull-requests';
import {assertNotMissingOrEmpty, getPrInfoFromDownloadPath, Logger} from '../common/utils';
// Classes
export class BuildCleaner {
private logger = new Logger('BuildCleaner');
// Constructor
constructor(protected buildsDir: string, protected githubOrg: string, protected githubRepo: string,
protected githubToken: string, protected downloadsDir: string, protected artifactPath: string) {
assertNotMissingOrEmpty('buildsDir', buildsDir);
assertNotMissingOrEmpty('githubOrg', githubOrg);
assertNotMissingOrEmpty('githubRepo', githubRepo);
assertNotMissingOrEmpty('githubToken', githubToken);
assertNotMissingOrEmpty('downloadsDir', downloadsDir);
assertNotMissingOrEmpty('artifactPath', artifactPath);
}
// Methods - Public
public async cleanUp(): Promise<void> {
try {
this.logger.log('Cleaning up builds and downloads');
const openPrs = await this.getOpenPrNumbers();
this.logger.log(`Open pull requests: ${openPrs.length}`);
await Promise.all([
this.cleanBuilds(openPrs),
this.cleanDownloads(openPrs),
]);
} catch (error) {
this.logger.error('ERROR:', error);
throw error;
}
}
public async cleanBuilds(openPrs: number[]): Promise<void> {
const existingBuilds = await this.getExistingBuildNumbers();
await this.removeUnnecessaryBuilds(existingBuilds, openPrs);
}
public async cleanDownloads(openPrs: number[]): Promise<void> {
const existingDownloads = await this.getExistingDownloads();
await this.removeUnnecessaryDownloads(existingDownloads, openPrs);
}
public getExistingBuildNumbers(): Promise<number[]> {
return new Promise<number[]>((resolve, reject) => {
fs.readdir(this.buildsDir, (err, files) => {
if (err) {
return reject(err);
}
const buildNumbers = files.
map(name => name.replace(HIDDEN_DIR_PREFIX, '')). // Remove the "hidden dir" prefix
map(Number). // Convert string to number
filter(Boolean); // Ignore NaN (or 0), because they are not builds
resolve(buildNumbers);
});
});
}
public async getOpenPrNumbers(): Promise<number[]> {
const api = new GithubApi(this.githubToken);
const githubPullRequests = new GithubPullRequests(api, this.githubOrg, this.githubRepo);
const prs = await githubPullRequests.fetchAll('open');
return prs.map(pr => pr.number);
}
public removeDir(dir: string): void {
try {
if (shell.test('-d', dir)) {
shell.chmod('-R', 'a+w', dir);
shell.rm('-rf', dir);
}
} catch (err) {
this.logger.error(`ERROR: Unable to remove '${dir}' due to:`, err);
}
}
public removeUnnecessaryBuilds(existingBuildNumbers: number[], openPrNumbers: number[]): void {
const toRemove = existingBuildNumbers.filter(num => !openPrNumbers.includes(num));
this.logger.log(`Existing builds: ${existingBuildNumbers.length}`);
this.logger.log(`Removing ${toRemove.length} build(s): ${toRemove.join(', ')}`);
// Try removing public dirs.
toRemove.
map(num => path.join(this.buildsDir, String(num))).
forEach(dir => this.removeDir(dir));
// Try removing hidden dirs.
toRemove.
map(num => path.join(this.buildsDir, HIDDEN_DIR_PREFIX + String(num))).
forEach(dir => this.removeDir(dir));
}
public getExistingDownloads(): Promise<string[]> {
const artifactFile = path.basename(this.artifactPath);
return new Promise<string[]>((resolve, reject) => {
fs.readdir(this.downloadsDir, (err, files) => {
if (err) {
return reject(err);
}
files = files.filter(file => file.endsWith(artifactFile));
resolve(files);
});
});
}
public removeUnnecessaryDownloads(existingDownloads: string[], openPrNumbers: number[]): void {
const toRemove = existingDownloads.filter(filePath => {
const {pr} = getPrInfoFromDownloadPath(filePath);
return !openPrNumbers.includes(pr);
});
this.logger.log(`Existing downloads: ${existingDownloads.length}`);
this.logger.log(`Removing ${toRemove.length} download(s): ${toRemove.join(', ')}`);
toRemove.forEach(filePath => shell.rm(path.join(this.downloadsDir, filePath)));
}
}
@@ -0,0 +1,26 @@
// Imports
import {AIO_DOWNLOADS_DIR} from '../common/constants';
import {
AIO_ARTIFACT_PATH,
AIO_BUILDS_DIR,
AIO_GITHUB_ORGANIZATION,
AIO_GITHUB_REPO,
AIO_GITHUB_TOKEN,
} from '../common/env-variables';
import {BuildCleaner} from './build-cleaner';
// Run
_main();
// Functions
function _main(): void {
const buildCleaner = new BuildCleaner(
AIO_BUILDS_DIR,
AIO_GITHUB_ORGANIZATION,
AIO_GITHUB_REPO,
AIO_GITHUB_TOKEN,
AIO_DOWNLOADS_DIR,
AIO_ARTIFACT_PATH);
buildCleaner.cleanUp().catch(() => process.exit(1));
}
@@ -0,0 +1,151 @@
// Imports
import fetch, {RequestInit} from 'node-fetch';
import {assertNotMissingOrEmpty} from './utils';
// Constants
const CIRCLE_CI_BASE_API_URL = 'https://circleci.com/api/v2';
const CIRCLE_CI_BUILD_API_URL = `${CIRCLE_CI_BASE_API_URL}/project/gh`;
const CIRCLE_CI_PIPELINE_API_URL = `${CIRCLE_CI_BASE_API_URL}/pipeline`;
// Interfaces - Types
// API docs: https://circleci.com/docs/api/v2#operation/getJobArtifacts
// Example: https://circleci.com/api/v2/project/gh/angular/angular/1163941/artifacts
export interface ArtifactInfo {
/** The path of this build artifacts. */
path: string;
/** The full URL where this artifact can be downloaded from. */
url: string;
// There are other fields but they are not used in this code.
}
export type ArtifactResponse = {items: ArtifactInfo[]};
// API docs: https://circleci.com/docs/api/v2#operation/getJobDetails
// Example: https://circleci.com/api/v2/project/gh/angular/angular/job/1163941
export interface BuildInfo {
/** The job number. */
number: number;
/** The job name (e.g. `'aio_preview'`). */
name: string;
/** Info about the organization which the project related to this job belongs to. */
organization: {
name: string;
};
/** Info about the project related to this job. */
project: {
name: string;
};
/** Info about the [pipeline](https://circleci.com/docs/2.0/pipelines/) that this job is part of. */
pipeline: {
id: string;
};
// There are other fields but they are not used in this code.
}
// API docs: https://circleci.com/docs/api/v2#operation/getPipelineById
// https://circleci.com/api/v2/pipeline/356227c0-32f6-4f99-bfc2-3938db90a147
export interface PipelineInfo {
/** The pipeline ID. */
id: string;
/** Info related to/retrieved from the version control system provider (e.g. GitHub). */
vcs: {
/** The PR number. */
review_id: string;
/** The HEAD SHA. */
revision: string;
};
// There are other fields but they are not used in this code.
}
/**
* A Helper that can interact with the CircleCI API.
*/
export class CircleCiApi {
/**
* Construct a helper that can interact with the CircleCI REST API.
* @param githubOrg The Github organisation whose repos we want to access in CircleCI (e.g.
* angular).
* @param githubRepo The Github repo whose builds we want to access in CircleCI (e.g. angular).
* @param circleCiToken The CircleCI API access token (secret).
*/
constructor(
private githubOrg: string,
private githubRepo: string,
private circleCiToken: string,
) {
assertNotMissingOrEmpty('githubOrg', githubOrg);
assertNotMissingOrEmpty('githubRepo', githubRepo);
assertNotMissingOrEmpty('circleCiToken', circleCiToken);
}
public async fetchFromCircleCi(url: string, params: RequestInit = {}) {
params.headers = {...params.headers, 'Circle-Token': this.circleCiToken};
const response = await fetch(url, params);
if (response.status !== 200) {
throw new Error(`${url}: ${response.status} - ${response.statusText}`);
}
return response;
}
/**
* Get the info for a build (aka job) from the CircleCI API.
* @param buildNumber The CircleCI build number that generated the artifact.
* @returns A promise to the info about the build.
*/
public async getBuildInfo(buildNumber: number): Promise<BuildInfo> {
try {
const url = `${CIRCLE_CI_BUILD_API_URL}/${this.githubOrg}/${this.githubRepo}/job/${buildNumber}`;
const response = await this.fetchFromCircleCi(url);
return response.json();
} catch (error) {
throw new Error(`CircleCI build info request failed (${(error as Error).message})`);
}
}
/**
* Query the CircleCI API to get a URL for a specified artifact from a specified build.
* @param artifactPath The path, within the build to the artifact.
* @returns A promise to the URL that can be requested to download the actual build artifact file.
*/
public async getBuildArtifactUrl(buildNumber: number, artifactPath: string): Promise<string> {
const baseUrl = `${CIRCLE_CI_BUILD_API_URL}/${this.githubOrg}/${this.githubRepo}/${buildNumber}`;
try {
const response = await this.fetchFromCircleCi(`${baseUrl}/artifacts`);
const artifacts = await response.json() as ArtifactResponse;
const artifact = artifacts.items.find(item => item.path === artifactPath);
if (!artifact) {
throw new Error(`Missing artifact (${artifactPath}) for CircleCI build: ${buildNumber}`);
}
return artifact.url;
} catch (error) {
throw new Error(`CircleCI artifact URL request failed (${(error as Error).message})`);
}
}
/**
* Get the info for a [pipeline](https://circleci.com/docs/2.0/pipelines/) from the CircleCI API.
* @param pipelineId The CircleCI pipeline ID that generated the artifact.
* @returns A promise to the info about the pipeline.
*/
public async getPipelineInfo(pipelineId: string): Promise<PipelineInfo> {
try {
const url = `${CIRCLE_CI_PIPELINE_API_URL}/${pipelineId}`;
const response = await this.fetchFromCircleCi(url);
return response.json();
} catch (error) {
throw new Error(`CircleCI pipeline info request failed (${(error as Error).message})`);
}
}
}
@@ -0,0 +1,4 @@
// Constants
export const AIO_DOWNLOADS_DIR = '/tmp/aio-downloads';
export const HIDDEN_DIR_PREFIX = 'hidden--';
export const SHORT_SHA_LEN = 7;
@@ -0,0 +1,19 @@
import {getEnvVar} from './utils';
export const AIO_ARTIFACT_PATH = getEnvVar('AIO_ARTIFACT_PATH');
export const AIO_BUILDS_DIR = getEnvVar('AIO_BUILDS_DIR');
export const AIO_GITHUB_TOKEN = getEnvVar('AIO_GITHUB_TOKEN');
export const AIO_CIRCLE_CI_TOKEN = getEnvVar('AIO_CIRCLE_CI_TOKEN');
export const AIO_DOMAIN_NAME = getEnvVar('AIO_DOMAIN_NAME');
export const AIO_GITHUB_ORGANIZATION = getEnvVar('AIO_GITHUB_ORGANIZATION');
export const AIO_GITHUB_REPO = getEnvVar('AIO_GITHUB_REPO');
export const AIO_GITHUB_TEAM_SLUGS = getEnvVar('AIO_GITHUB_TEAM_SLUGS');
export const AIO_NGINX_HOSTNAME = getEnvVar('AIO_NGINX_HOSTNAME');
export const AIO_NGINX_PORT_HTTP = +getEnvVar('AIO_NGINX_PORT_HTTP');
export const AIO_NGINX_PORT_HTTPS = +getEnvVar('AIO_NGINX_PORT_HTTPS');
export const AIO_SIGNIFICANT_FILES_PATTERN = getEnvVar('AIO_SIGNIFICANT_FILES_PATTERN');
export const AIO_TRUSTED_PR_LABEL = getEnvVar('AIO_TRUSTED_PR_LABEL');
export const AIO_PREVIEW_SERVER_HOSTNAME = getEnvVar('AIO_PREVIEW_SERVER_HOSTNAME');
export const AIO_PREVIEW_SERVER_PORT = +getEnvVar('AIO_PREVIEW_SERVER_PORT');
export const AIO_ARTIFACT_MAX_SIZE = +getEnvVar('AIO_ARTIFACT_MAX_SIZE');
export const AIO_WWW_USER = getEnvVar('AIO_WWW_USER');
@@ -0,0 +1,111 @@
// Imports
import {IncomingMessage} from 'http';
import * as https from 'https';
import {assertNotMissingOrEmpty} from './utils';
// Constants
const GITHUB_HOSTNAME = 'api.github.com';
// Interfaces - Types
interface RequestParams {
[key: string]: string | number;
}
type RequestParamsOrNull = RequestParams | null;
// Classes
export class GithubApi {
protected requestHeaders: {[key: string]: string};
// Constructor
constructor(githubToken: string) {
assertNotMissingOrEmpty('githubToken', githubToken);
this.requestHeaders = {
'Authorization': `token ${githubToken}`,
'User-Agent': `Node/${process.versions.node}`,
};
}
// Methods - Public
public get<T = any>(pathname: string, params?: RequestParamsOrNull): Promise<T> {
const path = this.buildPath(pathname, params);
return this.request<T>('get', path);
}
public post<T = any>(pathname: string, params?: RequestParamsOrNull, data?: any): Promise<T> {
const path = this.buildPath(pathname, params);
return this.request<T>('post', path, data);
}
// In GitHub API paginated requests, page numbering is 1-based. (https://developer.github.com/v3/#pagination)
public getPaginated<T>(pathname: string, baseParams: RequestParams = {}, currentPage: number = 1): Promise<T[]> {
const perPage = 100;
const params = {
...baseParams,
page: currentPage,
per_page: perPage,
};
return this.get<T[]>(pathname, params).then(items => {
if (items.length < perPage) {
return items;
}
return this.getPaginated<T>(pathname, baseParams, currentPage + 1).then(moreItems => [...items, ...moreItems]);
});
}
// Methods - Protected
protected buildPath(pathname: string, params?: RequestParamsOrNull): string {
if (params == null) {
return pathname;
}
const search = (params === null) ? '' : this.serializeSearchParams(params);
const joiner = search && '?';
return `${pathname}${joiner}${search}`;
}
protected request<T>(method: string, path: string, data: any = null): Promise<T> {
return new Promise<T>((resolve, reject) => {
const options = {
headers: {...this.requestHeaders},
host: GITHUB_HOSTNAME,
method,
path,
};
const onError = (statusCode: number, responseText: string) => {
const url = `https://${GITHUB_HOSTNAME}${path}`;
reject(`Request to '${url}' failed (status: ${statusCode}): ${responseText}`);
};
const onSuccess = (responseText: string) => {
try { resolve(responseText && JSON.parse(responseText)); } catch (err) { reject(err); }
};
const onResponse = (res: IncomingMessage) => {
const statusCode = res.statusCode || -1;
const isSuccess = (200 <= statusCode) && (statusCode < 400);
let responseText = '';
res.
on('data', d => responseText += d).
on('end', () => isSuccess ? onSuccess(responseText) : onError(statusCode, responseText)).
on('error', reject);
};
https.
request(options, onResponse).
on('error', reject).
end(data && JSON.stringify(data));
});
}
protected serializeSearchParams(params: RequestParams): string {
return Object.keys(params).
filter(key => params[key] != null).
map(key => `${key}=${encodeURIComponent(String(params[key]))}`).
join('&');
}
}
@@ -0,0 +1,79 @@
import {GithubApi} from './github-api';
import {assert, assertNotMissingOrEmpty} from './utils';
export interface PullRequest {
number: number;
user: {login: string};
labels: {name: string}[];
}
export interface FileInfo {
sha: string;
filename: string;
}
export type PullRequestState = 'all' | 'closed' | 'open';
/**
* Access pull requests on GitHub.
*/
export class GithubPullRequests {
public repoSlug: string;
/**
* Create an instance of this helper
* @param api An instance of the Github API helper.
* @param githubOrg The organisation on GitHub whose repo we will interrogate.
* @param githubRepo The repository on Github with whose PRs we will interact.
*/
constructor(private api: GithubApi, githubOrg: string, githubRepo: string) {
assertNotMissingOrEmpty('githubOrg', githubOrg);
assertNotMissingOrEmpty('githubRepo', githubRepo);
this.repoSlug = `${githubOrg}/${githubRepo}`;
}
/**
* Post a comment on a PR.
* @param pr The number of the PR on which to comment.
* @param body The body of the comment to post.
* @returns A promise that resolves when the comment has been posted.
*/
public addComment(pr: number, body: string): Promise<any> {
assert(pr > 0, `Invalid PR number: ${pr}`);
assert(!!body, `Invalid or empty comment body: ${body}`);
return this.api.post<any>(`/repos/${this.repoSlug}/issues/${pr}/comments`, null, {body});
}
/**
* Request information about a PR.
* @param pr The number of the PR for which to request info.
* @returns A promise that is resolves with information about the specified PR.
*/
public fetch(pr: number): Promise<PullRequest> {
assert(pr > 0, `Invalid PR number: ${pr}`);
// Using the `/issues/` URL, because the `/pulls/` one does not provide labels.
return this.api.get<PullRequest>(`/repos/${this.repoSlug}/issues/${pr}`);
}
/**
* Request information about all PRs that match the given state.
* @param state Only retrieve PRs that have this state.
* @returns A promise that is resolved with information about the requested PRs.
*/
public fetchAll(state: PullRequestState = 'all'): Promise<PullRequest[]> {
const pathname = `/repos/${this.repoSlug}/pulls`;
const params = {state};
return this.api.getPaginated<PullRequest>(pathname, params);
}
/**
* Request a list of files for the given PR.
* @param pr The number of the PR for which to request files.
* @returns A promise that resolves to an array of file information
*/
public fetchFiles(pr: number): Promise<FileInfo[]> {
assert(pr > 0, `Invalid PR number: ${pr}`);
return this.api.getPaginated<FileInfo>(`/repos/${this.repoSlug}/pulls/${pr}/files`);
}
}
@@ -0,0 +1,72 @@
import {GithubApi} from './github-api';
import {assertNotMissingOrEmpty} from './utils';
export interface Team {
id: number;
slug: string;
}
export interface TeamMembership {
state: string;
}
export class GithubTeams {
/**
* Create an instance of this helper
* @param api An instance of the Github API helper.
* @param githubOrg The organisation on GitHub whose repo we will interrogate.
*/
constructor(private api: GithubApi, protected githubOrg: string) {
assertNotMissingOrEmpty('githubOrg', githubOrg);
}
/**
* Request information about all the organisation's teams in GitHub.
* @returns A promise that is resolved with information about the teams.
*/
public fetchAll(): Promise<Team[]> {
return this.api.getPaginated<Team>(`/orgs/${this.githubOrg}/teams`);
}
/**
* Check whether the specified username is a member of the specified team.
* @param username The usernane to check for in the team.
* @param teamIds The team to check for the username.
* @returns a Promise that resolves to `true` if the username is a member of the team.
*/
public async isMemberById(username: string, teamIds: number[]): Promise<boolean> {
const getMembership = async (teamId: number) => {
try {
const {state} = await this.api.get<TeamMembership>(`/teams/${teamId}/memberships/${username}`);
return state === 'active';
} catch (error) {
return false;
}
};
for (const teamId of teamIds) {
if (await getMembership(teamId)) {
return true;
}
}
return false;
}
/**
* Check whether the given username is a member of the teams specified by the team slugs.
* @param username The username to check for in the teams.
* @param teamSlugs A collection of slugs that represent the teams to check for the username.
* @returns a Promise that resolves to `true` if the usernane is a member of at least one of the specified teams.
*/
public async isMemberBySlug(username: string, teamSlugs: string[]): Promise<boolean> {
try {
const teams = await this.fetchAll();
const teamIds = teams.filter(team => teamSlugs.includes(team.slug)).map(team => team.id);
return await this.isMemberById(username, teamIds);
} catch (error) {
return false;
}
}
}
@@ -0,0 +1,22 @@
// We can't use `import...from` here, because of the following mess:
// - GitHub project `jasmine/jasmine` is `jasmine-core` on npm and its typings `@types/jasmine`.
// - GitHub project `jasmine/jasmine-npm` is `jasmine` on npm and has no typings.
//
// Using `import...from 'jasmine'` here, would import from `@types/jasmine` (which refers to the
// `jasmine-core` module and the `jasmine` module).
import Jasmine = require('jasmine');
import 'source-map-support/register';
export const runTests = (specFiles: string[]) => {
const config = {
random: true,
spec_files: specFiles,
stopSpecOnExpectationFailure: true,
};
process.on('unhandledRejection', (reason: any) => console.log('Unhandled rejection:', reason));
const runner = new Jasmine({});
runner.loadConfig(config);
runner.execute();
};
@@ -0,0 +1,109 @@
import {basename, resolve as resolvePath} from 'path';
import {SHORT_SHA_LEN} from './constants';
/**
* Shorten a SHA to make it more readable
* @param sha The SHA to shorten.
*/
export function computeShortSha(sha: string) {
return sha.slice(0, SHORT_SHA_LEN);
}
/**
* Compute the path for a downloaded artifact file.
* @param downloadsDir The directory where artifacts are downloaded
* @param pr The PR associated with this artifact.
* @param sha The SHA associated with the build for this artifact.
* @param artifactPath The path to the artifact on CircleCI.
* @returns The fully resolved location for the specified downloaded artifact.
*/
export function computeArtifactDownloadPath(
downloadsDir: string, pr: number, sha: string, artifactPath: string) {
return resolvePath(downloadsDir, `${pr}-${computeShortSha(sha)}-${basename(artifactPath)}`);
}
/**
* Extract the PR number and latest commit SHA from a downloaded file path.
* @param downloadPath the path to the downloaded file.
* @returns An object whose keys are the PR and SHA extracted from the file path.
*/
export function getPrInfoFromDownloadPath(downloadPath: string) {
const file = basename(downloadPath);
const [pr, sha] = file.split('-');
return {pr: +pr, sha};
}
/**
* Assert that a value is true.
* @param value The value to assert.
* @param message The message if the value is not true.
*/
export function assert(value: boolean, message: string) {
if (!value) {
throw new Error(message);
}
}
/**
* Assert that a parameter is not equal to "".
* @param name The name of the parameter.
* @param value The value of the parameter.
*/
export const assertNotMissingOrEmpty = (name: string, value: string|null|undefined) => {
assert(!!value, `Missing or empty required parameter '${name}'!`);
};
/**
* Get an environment variable.
* @param name The name of the environment variable.
* @param isOptional True if the variable is optional.
* @returns The value of the variable or "" if it is optional and falsy.
* @throws `Error` if the variable is falsy and not optional.
*/
export const getEnvVar = (name: string, isOptional = false): string => {
const value = process.env[name];
if (!isOptional && !value) {
try {
throw new Error(`ERROR: Missing required environment variable '${name}'!`);
} catch (error) {
console.error((error as Error).stack);
process.exit(1);
}
}
return value || '';
};
/**
* A basic logger implementation.
* Delegates to `console`, but prepends each message with the current date and specified scope (i.e
* caller).
*/
export class Logger {
private padding = ' '.repeat(20 - this.scope.length);
/**
* Create a new `Logger` instance for the specified `scope`.
* @param scope The logger's scope (added to all messages).
*/
constructor(private scope: string) {}
public error(...args: any[]) {
this.callMethod('error', args);
}
public info(...args: any[]) {
this.callMethod('info', args);
}
public log(...args: any[]) {
this.callMethod('log', args);
}
public warn(...args: any[]) {
this.callMethod('warn', args);
}
private callMethod(method: 'error'|'info'|'log'|'warn', args: any[]) {
console[method](`[${new Date()}]`, `${this.scope}:${this.padding}`, ...args);
}
}
@@ -0,0 +1,144 @@
// Imports
import * as cp from 'child_process';
import {EventEmitter} from 'events';
import * as fs from 'fs';
import * as path from 'path';
import * as shell from 'shelljs';
import {HIDDEN_DIR_PREFIX} from '../common/constants';
import {assertNotMissingOrEmpty, computeShortSha, Logger} from '../common/utils';
import {ChangedPrVisibilityEvent, CreatedBuildEvent} from './build-events';
import {PreviewServerError} from './preview-error';
// Classes
export class BuildCreator extends EventEmitter {
private logger = new Logger('BuildCreator');
// Constructor
constructor(protected buildsDir: string) {
super();
assertNotMissingOrEmpty('buildsDir', buildsDir);
}
// Methods - Public
public create(pr: number, sha: string, archivePath: string, isPublic: boolean): Promise<void> {
// Use only part of the SHA for more readable URLs.
sha = computeShortSha(sha);
const {newPrDir: prDir} = this.getCandidatePrDirs(pr, isPublic);
const shaDir = path.join(prDir, sha);
let dirToRemoveOnError: string;
return Promise.resolve().
// If the same PR exists with different visibility, update the visibility first.
then(() => this.updatePrVisibility(pr, isPublic)).
then(() => Promise.all([this.exists(prDir), this.exists(shaDir)])).
then(([prDirExisted, shaDirExisted]) => {
if (shaDirExisted) {
const publicOrNot = isPublic ? 'public' : 'non-public';
throw new PreviewServerError(409, `Request to overwrite existing ${publicOrNot} directory: ${shaDir}`);
}
dirToRemoveOnError = prDirExisted ? shaDir : prDir;
return Promise.resolve().
then(() => shell.mkdir('-p', shaDir)).
then(() => this.extractArchive(archivePath, shaDir)).
then(() => this.emit(CreatedBuildEvent.type, new CreatedBuildEvent(+pr, sha, isPublic))).
then(() => undefined);
}).
catch(err => {
if (dirToRemoveOnError) {
shell.rm('-rf', dirToRemoveOnError);
}
if (!(err instanceof PreviewServerError)) {
err = new PreviewServerError(500, `Error while creating preview at: ${shaDir}\n${err}`);
}
throw err;
});
}
public updatePrVisibility(pr: number, makePublic: boolean): Promise<boolean> {
const {oldPrDir: otherVisPrDir, newPrDir: targetVisPrDir} = this.getCandidatePrDirs(pr, makePublic);
return Promise.
all([this.exists(otherVisPrDir), this.exists(targetVisPrDir)]).
then(([otherVisPrDirExisted, targetVisPrDirExisted]) => {
if (!otherVisPrDirExisted) {
// No visibility change: Either the visibility is up-to-date or the PR does not exist.
return false;
} else if (targetVisPrDirExisted) {
// Error: Directories for both visibilities exist.
throw new PreviewServerError(409,
`Request to move '${otherVisPrDir}' to existing directory '${targetVisPrDir}'.`);
}
// Visibility change: Moving `otherVisPrDir` to `targetVisPrDir`.
return Promise.resolve().
then(() => shell.mv(otherVisPrDir, targetVisPrDir)).
then(() => this.listShasByDate(targetVisPrDir)).
then(shas => this.emit(ChangedPrVisibilityEvent.type, new ChangedPrVisibilityEvent(+pr, shas, makePublic))).
then(() => true);
}).
catch(err => {
if (!(err instanceof PreviewServerError)) {
err = new PreviewServerError(500, `Error while making PR ${pr} ${makePublic ? 'public' : 'hidden'}.\n${err}`);
}
throw err;
});
}
// Methods - Protected
protected exists(fileOrDir: string): Promise<boolean> {
return new Promise(resolve => fs.access(fileOrDir, err => resolve(!err)));
}
protected extractArchive(inputFile: string, outputDir: string): Promise<void> {
return new Promise<void>((resolve, reject) => {
const cmd = `tar --extract --gzip --directory "${outputDir}" --file "${inputFile}"`;
cp.exec(cmd, (err, _stdout, stderr) => {
if (err) {
return reject(err);
}
if (stderr) {
this.logger.warn(stderr);
}
try {
shell.chmod('-R', 'a-w', outputDir);
shell.rm('-f', inputFile);
resolve();
} catch (err) {
reject(err);
}
});
});
}
protected getCandidatePrDirs(pr: number, isPublic: boolean): {oldPrDir: string, newPrDir: string} {
const hiddenPrDir = path.join(this.buildsDir, HIDDEN_DIR_PREFIX + pr);
const publicPrDir = path.join(this.buildsDir, `${pr}`);
const oldPrDir = isPublic ? hiddenPrDir : publicPrDir;
const newPrDir = isPublic ? publicPrDir : hiddenPrDir;
return {oldPrDir, newPrDir};
}
protected listShasByDate(inputDir: string): Promise<string[]> {
return Promise.resolve().
then(() => shell.ls('-l', inputDir) as any as Promise<(fs.Stats & {name: string})[]>).
// Keep directories only.
// (Also, convert to standard Array - ShellJS provides custom `sort()` method for sorting file contents.)
then(items => items.filter(item => item.isDirectory())).
// Sort by modification date.
then(items => items.sort((a, b) => a.mtime.getTime() - b.mtime.getTime())).
// Return directory names.
then(items => items.map(item => item.name));
}
}
@@ -0,0 +1,16 @@
// Classes
export class ChangedPrVisibilityEvent {
// Properties - Public, Static
public static type = 'pr.changedVisibility';
// Constructor
constructor(public pr: number, public shas: string[], public isPublic: boolean) {}
}
export class CreatedBuildEvent {
// Properties - Public, Static
public static type = 'build.created';
// Constructor
constructor(public pr: number, public sha: string, public isPublic: boolean) {}
}
@@ -0,0 +1,77 @@
import * as fs from 'fs';
import {dirname} from 'path';
import {mkdir} from 'shelljs';
import {promisify} from 'util';
import {CircleCiApi} from '../common/circle-ci-api';
import {assert, assertNotMissingOrEmpty, computeArtifactDownloadPath, Logger} from '../common/utils';
import {PreviewServerError} from './preview-error';
export interface GithubInfo {
org: string;
pr: number;
repo: string;
sha: string;
}
/**
* A helper that can get information about builds and download build artifacts.
*/
export class BuildRetriever {
private logger = new Logger('BuildRetriever');
constructor(
private api: CircleCiApi, private downloadSizeLimit: number, private downloadDir: string) {
assert(
downloadSizeLimit > 0,
'Invalid parameter "downloadSizeLimit" should be a number greater than 0.');
assertNotMissingOrEmpty('downloadDir', downloadDir);
}
/**
* Get GitHub information about a build
* @param buildNum The number of the build for which to retrieve the info.
* @returns The Github org, repo, PR and latest SHA for the specified build.
*/
public async getGithubInfo(buildNum: number): Promise<GithubInfo> {
const buildInfo = await this.api.getBuildInfo(buildNum);
const pipelineInfo = await this.api.getPipelineInfo(buildInfo.pipeline.id);
const githubInfo: GithubInfo = {
org: buildInfo.organization.name,
pr: +pipelineInfo.vcs.review_id,
repo: buildInfo.project.name,
sha: pipelineInfo.vcs.revision,
};
return githubInfo;
}
/**
* Make a request to the given URL for a build artifact and store it locally.
* @param buildNum the number of the CircleCI build whose artifact we want to download.
* @param pr the number of the PR that triggered the CircleCI build.
* @param sha the commit in the PR that triggered the CircleCI build.
* @param artifactPath the path on CircleCI where the artifact was stored.
* @returns A promise to the file path where the downloaded file was stored.
*/
public async downloadBuildArtifact(
buildNum: number, pr: number, sha: string, artifactPath: string): Promise<string> {
try {
const outPath = computeArtifactDownloadPath(this.downloadDir, pr, sha, artifactPath);
const downloadExists =
await new Promise(resolve => fs.exists(outPath, exists => resolve(exists)));
if (!downloadExists) {
const url = await this.api.getBuildArtifactUrl(buildNum, artifactPath);
const response = await this.api.fetchFromCircleCi(url, {size: this.downloadSizeLimit});
const buffer = await response.buffer();
mkdir('-p', dirname(outPath));
await promisify(fs.writeFile)(outPath, buffer);
}
return outPath;
} catch (error: any) {
this.logger.warn(error);
const status = (error.type === 'max-size') ? 413 : 500;
throw new PreviewServerError(
status, `CircleCI artifact download failed (${error.message || error})`);
}
}
}
@@ -0,0 +1,46 @@
import {GithubPullRequests, PullRequest} from '../common/github-pull-requests';
import {GithubTeams} from '../common/github-teams';
import {assertNotMissingOrEmpty} from '../common/utils';
/**
* A helper to verify whether builds are trusted.
*/
export class BuildVerifier {
/**
* Construct a new BuildVerifier instance.
* @param prs A helper to access PR information.
* @param teams A helper to access Github team information.
* @param allowedTeamSlugs The teams that are trusted.
* @param trustedPrLabel The github label that indicates that a PR is trusted.
*/
constructor(protected prs: GithubPullRequests, protected teams: GithubTeams,
protected allowedTeamSlugs: string[], protected trustedPrLabel: string) {
assertNotMissingOrEmpty('allowedTeamSlugs', allowedTeamSlugs && allowedTeamSlugs.join(''));
assertNotMissingOrEmpty('trustedPrLabel', trustedPrLabel);
}
/**
* Check whether a PR contains files that are significant to the build.
* @param pr The number of the PR to check
* @param significantFilePattern A regex that selects files that are significant.
*/
public async getSignificantFilesChanged(pr: number, significantFilePattern: RegExp): Promise<boolean> {
const files = await this.prs.fetchFiles(pr);
return files.some(file => significantFilePattern.test(file.filename));
}
/**
* Check whether a PR is trusted.
* @param pr The number of the PR to check.
* @returns true if the PR is trusted.
*/
public async getPrIsTrusted(pr: number): Promise<boolean> {
const prInfo = await this.prs.fetch(pr);
return this.hasLabel(prInfo, this.trustedPrLabel) ||
(await this.teams.isMemberBySlug(prInfo.user.login, this.allowedTeamSlugs));
}
protected hasLabel(prInfo: PullRequest, label: string): boolean {
return prInfo.labels.some(labelObj => labelObj.name === label);
}
}
@@ -0,0 +1,41 @@
// Imports
import {AIO_DOWNLOADS_DIR} from '../common/constants';
import {
AIO_ARTIFACT_MAX_SIZE,
AIO_ARTIFACT_PATH,
AIO_BUILDS_DIR,
AIO_CIRCLE_CI_TOKEN,
AIO_DOMAIN_NAME,
AIO_GITHUB_ORGANIZATION,
AIO_GITHUB_REPO,
AIO_GITHUB_TEAM_SLUGS,
AIO_GITHUB_TOKEN,
AIO_PREVIEW_SERVER_HOSTNAME,
AIO_PREVIEW_SERVER_PORT,
AIO_SIGNIFICANT_FILES_PATTERN,
AIO_TRUSTED_PR_LABEL,
} from '../common/env-variables';
import {PreviewServerFactory} from './preview-server-factory';
// Run
_main();
// Functions
function _main(): void {
PreviewServerFactory
.create({
buildArtifactPath: AIO_ARTIFACT_PATH,
buildsDir: AIO_BUILDS_DIR,
circleCiToken: AIO_CIRCLE_CI_TOKEN,
domainName: AIO_DOMAIN_NAME,
downloadSizeLimit: AIO_ARTIFACT_MAX_SIZE,
downloadsDir: AIO_DOWNLOADS_DIR,
githubOrg: AIO_GITHUB_ORGANIZATION,
githubRepo: AIO_GITHUB_REPO,
githubTeamSlugs: AIO_GITHUB_TEAM_SLUGS.split(','),
githubToken: AIO_GITHUB_TOKEN,
significantFilesPattern: AIO_SIGNIFICANT_FILES_PATTERN,
trustedPrLabel: AIO_TRUSTED_PR_LABEL,
})
.listen(AIO_PREVIEW_SERVER_PORT, AIO_PREVIEW_SERVER_HOSTNAME);
}
@@ -0,0 +1,8 @@
// Classes
export class PreviewServerError extends Error {
// Constructor
constructor(public status: number = 500, message?: string) {
super(message);
Object.setPrototypeOf(this, PreviewServerError.prototype);
}
}
@@ -0,0 +1,207 @@
// Imports
import * as bodyParser from 'body-parser';
import * as express from 'express';
import * as http from 'http';
import {AddressInfo} from 'net';
import {CircleCiApi} from '../common/circle-ci-api';
import {GithubApi} from '../common/github-api';
import {GithubPullRequests} from '../common/github-pull-requests';
import {GithubTeams} from '../common/github-teams';
import {assert, assertNotMissingOrEmpty, computeShortSha, Logger} from '../common/utils';
import {BuildCreator} from './build-creator';
import {ChangedPrVisibilityEvent, CreatedBuildEvent} from './build-events';
import {BuildRetriever} from './build-retriever';
import {BuildVerifier} from './build-verifier';
import {respondWithError, throwRequestError} from './utils';
const AIO_PREVIEW_JOB = 'aio_preview';
// Interfaces - Types
export interface PreviewServerConfig {
downloadsDir: string;
downloadSizeLimit: number;
buildArtifactPath: string;
buildsDir: string;
domainName: string;
githubOrg: string;
githubRepo: string;
githubTeamSlugs: string[];
circleCiToken: string;
githubToken: string;
significantFilesPattern: string;
trustedPrLabel: string;
}
const logger = new Logger('PreviewServer');
// Classes
export class PreviewServerFactory {
// Methods - Public
public static create(cfg: PreviewServerConfig): http.Server {
assertNotMissingOrEmpty('domainName', cfg.domainName);
const circleCiApi = new CircleCiApi(cfg.githubOrg, cfg.githubRepo, cfg.circleCiToken);
const githubApi = new GithubApi(cfg.githubToken);
const prs = new GithubPullRequests(githubApi, cfg.githubOrg, cfg.githubRepo);
const teams = new GithubTeams(githubApi, cfg.githubOrg);
const buildRetriever = new BuildRetriever(circleCiApi, cfg.downloadSizeLimit, cfg.downloadsDir);
const buildVerifier = new BuildVerifier(prs, teams, cfg.githubTeamSlugs, cfg.trustedPrLabel);
const buildCreator = PreviewServerFactory.createBuildCreator(prs, cfg.buildsDir, cfg.domainName);
const middleware = PreviewServerFactory.createMiddleware(buildRetriever, buildVerifier, buildCreator, cfg);
const httpServer = http.createServer(middleware as any);
httpServer.on('listening', () => {
const info = httpServer.address() as AddressInfo;
logger.info(`Up and running (and listening on ${info.address}:${info.port})...`);
});
return httpServer;
}
public static createMiddleware(buildRetriever: BuildRetriever, buildVerifier: BuildVerifier,
buildCreator: BuildCreator, cfg: PreviewServerConfig): express.Express {
const middleware = express();
const jsonParser = bodyParser.json();
const significantFilesRe = new RegExp(cfg.significantFilesPattern);
// RESPOND TO IS-ALIVE PING
middleware.get(/^\/health-check\/?$/, (_req, res) => res.sendStatus(200));
// RESPOND TO CAN-HAVE-PUBLIC-PREVIEW CHECK
const canHavePublicPreviewRe = /^\/can-have-public-preview\/(\d+)\/?$/;
middleware.get(canHavePublicPreviewRe, async (req, res) => {
try {
const pr = +canHavePublicPreviewRe.exec(req.url)![1];
if (!await buildVerifier.getSignificantFilesChanged(pr, significantFilesRe)) {
// Cannot have preview: PR did not touch relevant files: `aio/` or `packages/` (except for spec files).
res.send({canHavePublicPreview: false, reason: 'No significant files touched.'});
logger.log(`PR:${pr} - Cannot have a public preview, because it did not touch any significant files.`);
} else if (!await buildVerifier.getPrIsTrusted(pr)) {
// Cannot have preview: PR not automatically verifiable as "trusted".
res.send({canHavePublicPreview: false, reason: 'Not automatically verifiable as "trusted".'});
logger.log(`PR:${pr} - Cannot have a public preview, because not automatically verifiable as "trusted".`);
} else {
// Can have preview.
res.send({canHavePublicPreview: true, reason: null});
logger.log(`PR:${pr} - Can have a public preview.`);
}
} catch (err) {
logger.error('Previewability check error', err);
respondWithError(res, err);
}
});
// CIRCLE_CI BUILD COMPLETE WEBHOOK
middleware.post(/^\/circle-build\/?$/, jsonParser, async (req, res) => {
try {
if (!(
req.is('json') &&
req.body &&
req.body.payload &&
req.body.payload.build_num > 0 &&
req.body.payload.build_parameters &&
req.body.payload.build_parameters.CIRCLE_JOB
)) {
throwRequestError(400, `Incorrect body content. Expected JSON`, req);
}
const job = req.body.payload.build_parameters.CIRCLE_JOB;
const buildNum = req.body.payload.build_num;
logger.log(`Build:${buildNum}, Job:${job} - processing web-hook trigger`);
if (job !== AIO_PREVIEW_JOB) {
res.sendStatus(204);
logger.log(`Build:${buildNum}, Job:${job} -`,
`Skipping preview processing because this is not the "${AIO_PREVIEW_JOB}" job.`);
return;
}
const { pr, sha, org, repo } = await buildRetriever.getGithubInfo(buildNum);
assert(cfg.githubOrg === org,
`Invalid webhook: expected "githubOrg" property to equal "${cfg.githubOrg}" but got "${org}".`);
assert(cfg.githubRepo === repo,
`Invalid webhook: expected "githubRepo" property to equal "${cfg.githubRepo}" but got "${repo}".`);
// Do not deploy unless this PR has touched relevant files: `aio/` or `packages/` (except for spec files)
if (!await buildVerifier.getSignificantFilesChanged(pr, significantFilesRe)) {
res.sendStatus(204);
logger.log(`PR:${pr}, Build:${buildNum} - ` +
`Skipping preview processing because this PR did not touch any significant files.`);
return;
}
const artifactPath = await buildRetriever.downloadBuildArtifact(buildNum, pr, sha, cfg.buildArtifactPath);
const isPublic = await buildVerifier.getPrIsTrusted(pr);
await buildCreator.create(pr, sha, artifactPath, isPublic);
res.sendStatus(isPublic ? 201 : 202);
logger.log(`PR:${pr}, SHA:${computeShortSha(sha)}, Build:${buildNum} - ` +
`Successfully created ${isPublic ? 'public' : 'non-public'} preview.`);
} catch (err) {
logger.error('CircleCI webhook error', err);
respondWithError(res, err);
}
});
// GITHUB PR UPDATED WEBHOOK
middleware.post(/^\/pr-updated\/?$/, jsonParser, async (req, res) => {
const { action, number: prNo }: { action?: string, number?: number } = req.body;
const visMayHaveChanged = !action || (action === 'labeled') || (action === 'unlabeled');
try {
if (!visMayHaveChanged) {
res.sendStatus(200);
} else if (!prNo) {
throwRequestError(400, `Missing or empty 'number' field`, req);
} else {
const isPublic = await buildVerifier.getPrIsTrusted(prNo);
await buildCreator.updatePrVisibility(prNo, isPublic);
res.sendStatus(200);
}
} catch (err) {
logger.error('PR update hook error', err);
respondWithError(res, err);
}
});
// ALL OTHER REQUESTS
middleware.all('*', req => throwRequestError(404, 'Unknown resource', req));
middleware.use((err: any, _req: any, res: express.Response, _next: any) => {
const statusText = http.STATUS_CODES[err.status] || '???';
logger.error(`Preview server error: ${err.status} - ${statusText}:`, err.message);
respondWithError(res, err);
});
return middleware;
}
public static createBuildCreator(prs: GithubPullRequests, buildsDir: string, domainName: string): BuildCreator {
const buildCreator = new BuildCreator(buildsDir);
const postPreviewsComment = (pr: number, shas: string[]) => {
const body = shas.
map(sha => `You can preview ${sha} at https://pr${pr}-${sha}.${domainName}/.`).
join('\n');
return prs.addComment(pr, body);
};
buildCreator.on(CreatedBuildEvent.type, ({pr, sha, isPublic}: CreatedBuildEvent) => {
if (isPublic) {
postPreviewsComment(pr, [sha]);
}
});
buildCreator.on(ChangedPrVisibilityEvent.type, ({pr, shas, isPublic}: ChangedPrVisibilityEvent) => {
if (isPublic && shas.length) {
postPreviewsComment(pr, shas);
}
});
return buildCreator;
}
}
@@ -0,0 +1,28 @@
import * as express from 'express';
import {PreviewServerError} from './preview-error';
/**
* Update the response to report that an error has occurred.
* @param res The response to configure as an error.
* @param err The error that needs to be reported.
*/
export async function respondWithError(res: express.Response, err: any): Promise<void> {
if (!(err instanceof PreviewServerError)) {
err = new PreviewServerError(500, String((err && err.message) || err));
}
res.status(err.status);
return new Promise(resolve => res.end(err.message, resolve));
}
/**
* Throw an exception that describes the given error information.
* @param status The HTTP status code include in the error.
* @param error The error message to include in the error.
* @param req The request that triggered this error.
*/
export function throwRequestError(status: number, error: string, req: express.Request): never {
const message = `${error} in request: ${req.method} ${req.originalUrl}` +
(!req.body ? '' : ` ${JSON.stringify(req.body)}`);
throw new PreviewServerError(status, message);
}
@@ -0,0 +1,49 @@
export const enum BuildNums {
BUILD_INFO_ERROR = 1,
BUILD_INFO_404,
BUILD_INFO_INVALID_GH_ORG,
BUILD_INFO_INVALID_GH_REPO,
PIPELINE_INFO_ERROR,
PIPELINE_INFO_404,
CHANGED_FILES_ERROR,
CHANGED_FILES_404,
CHANGED_FILES_NONE,
BUILD_ARTIFACTS_ERROR,
BUILD_ARTIFACTS_404,
BUILD_ARTIFACTS_EMPTY,
BUILD_ARTIFACTS_MISSING,
DOWNLOAD_ARTIFACT_ERROR,
DOWNLOAD_ARTIFACT_404,
DOWNLOAD_ARTIFACT_TOO_BIG,
TRUST_CHECK_ERROR,
TRUST_CHECK_UNTRUSTED,
TRUST_CHECK_TRUSTED_LABEL,
TRUST_CHECK_ACTIVE_TRUSTED_USER,
TRUST_CHECK_INACTIVE_TRUSTED_USER,
}
export const enum PipelineIds {
PIPELINE_INFO_ERROR = 'pipeline-error',
PIPELINE_INFO_404 = 'pipeline-404',
CHANGED_FILES_ERROR = 'pipeline-cfe',
CHANGED_FILES_404 = 'pipeline-cf404',
CHANGED_FILES_NONE = 'pipeline-cfn',
TRUST_CHECK_ERROR = 'pipeline-tce',
TRUST_CHECK_UNTRUSTED = 'pipeline-tcu',
PIPELINE_INFO_OK = 'pipeline-ok',
}
export const enum PrNums {
CHANGED_FILES_ERROR = 1,
CHANGED_FILES_404,
CHANGED_FILES_NONE,
TRUST_CHECK_ERROR,
TRUST_CHECK_UNTRUSTED,
TRUST_CHECK_TRUSTED_LABEL,
TRUST_CHECK_ACTIVE_TRUSTED_USER,
TRUST_CHECK_INACTIVE_TRUSTED_USER,
}
export const SHA = '1234567890'.repeat(4);
export const ALT_SHA = 'abcde'.repeat(8);
export const SIMILAR_SHA = SHA.slice(0, -1) + 'A';
@@ -0,0 +1,10 @@
declare module 'delete-empty' {
interface Options {
dryRun: boolean;
verbose: boolean;
filter: (filePath: string) => boolean;
}
export default function deleteEmpty(cwd: string, options?: Options): Promise<string[]>;
export default function deleteEmpty(cwd: string, options?: Options, callback?: (err: any, deleted: string[]) => void): void;
export function sync(cwd: string, options?: Options): string[];
}
@@ -0,0 +1,237 @@
// Imports
import * as cp from 'child_process';
import * as fs from 'fs';
import * as path from 'path';
import * as shell from 'shelljs';
import {AIO_DOWNLOADS_DIR, HIDDEN_DIR_PREFIX} from '../common/constants';
import {
AIO_BUILDS_DIR,
AIO_NGINX_PORT_HTTP,
AIO_NGINX_PORT_HTTPS,
AIO_WWW_USER,
} from '../common/env-variables';
import {computeShortSha, Logger} from '../common/utils';
// Interfaces - Types
export interface CmdResult { success: boolean; err: Error | null; stdout: string; stderr: string; }
export interface FileSpecs { content?: string; size?: number; }
export type CleanUpFn = () => void;
export type TestSuiteFactory = (scheme: string, port: number) => void;
export type VerifyCmdResultFn = (result: CmdResult) => void;
// Classes
class Helper {
// Properties - Protected
protected cleanUpFns: CleanUpFn[] = [];
protected portPerScheme: {[scheme: string]: number} = {
http: AIO_NGINX_PORT_HTTP,
https: AIO_NGINX_PORT_HTTPS,
};
private logger = new Logger('TestHelper');
// Constructor
constructor() {
shell.mkdir('-p', AIO_BUILDS_DIR);
shell.exec(`chown -R ${AIO_WWW_USER} ${AIO_BUILDS_DIR}`);
shell.mkdir('-p', AIO_DOWNLOADS_DIR);
shell.exec(`chown -R ${AIO_WWW_USER} ${AIO_DOWNLOADS_DIR}`);
}
// Methods - Public
public cleanUp(): void {
while (this.cleanUpFns.length) {
// Clean-up fns remove themselves from the list.
this.cleanUpFns[0]();
}
const leftoverDownloads = fs.readdirSync(AIO_DOWNLOADS_DIR);
const leftoverBuilds = fs.readdirSync(AIO_BUILDS_DIR);
if (leftoverDownloads.length) {
this.logger.log(`Downloads directory '${AIO_DOWNLOADS_DIR}' is not empty after clean-up.`, leftoverDownloads);
shell.rm('-rf', `${AIO_DOWNLOADS_DIR}/*`);
}
if (leftoverBuilds.length) {
this.logger.log(`Builds directory '${AIO_BUILDS_DIR}' is not empty after clean-up.`, leftoverBuilds);
shell.rm('-rf', `${AIO_BUILDS_DIR}/*`);
}
if (leftoverBuilds.length || leftoverDownloads.length) {
throw new Error(`Unexpected test files not cleaned up.`);
}
}
public createDummyBuild(pr: number, sha: string, isPublic = true, force = false, legacy = false): CleanUpFn {
const prDir = this.getPrDir(pr, isPublic);
const shaDir = this.getShaDir(prDir, sha, legacy);
const idxPath = path.join(shaDir, 'index.html');
const barPath = path.join(shaDir, 'foo', 'bar.js');
this.writeFile(idxPath, {content: `PR: ${pr} | SHA: ${sha} | File: /index.html`}, force);
this.writeFile(barPath, {content: `PR: ${pr} | SHA: ${sha} | File: /foo/bar.js`}, force);
shell.exec(`chown -R ${AIO_WWW_USER} ${prDir}`);
return this.createCleanUpFn(() => shell.rm('-rf', prDir));
}
public getPrDir(pr: number, isPublic: boolean): string {
const prDirName = isPublic ? '' + pr : HIDDEN_DIR_PREFIX + pr;
return path.join(AIO_BUILDS_DIR, prDirName);
}
public getShaDir(prDir: string, sha: string, legacy = false): string {
return path.join(prDir, legacy ? sha : computeShortSha(sha));
}
public readBuildFile(pr: number, sha: string, relFilePath: string, isPublic = true, legacy = false): string {
const shaDir = this.getShaDir(this.getPrDir(pr, isPublic), sha, legacy);
const absFilePath = path.join(shaDir, relFilePath);
return fs.readFileSync(absFilePath, 'utf8');
}
public runCmd(cmd: string, opts: cp.ExecOptions = {}): Promise<CmdResult> {
return new Promise(resolve => {
const proc = cp.exec(cmd, opts, (err, stdout, stderr) => resolve({success: !err, err, stdout, stderr}));
this.createCleanUpFn(() => proc.kill());
});
}
public runForAllSupportedSchemes(suiteFactory: TestSuiteFactory): void {
Object.entries(this.portPerScheme).forEach(([scheme, port]) => suiteFactory(scheme, port));
}
public verifyResponse(status: number, regex: string | RegExp = /^/): VerifyCmdResultFn {
return (result: CmdResult) => {
const [headers, body] = result.stdout.
split(/(?:\r?\n){2,}/).
map(s => s.trim()).
slice(-2); // In case of redirect, discard the previous headers.
// Only keep the last to sections (final headers and body).
if (!result.success) {
this.logger.log('Stdout:', result.stdout);
this.logger.error('Stderr:', result.stderr);
this.logger.error('Error:', result.err);
}
expect(result.success).toBe(true);
expect(headers).toMatch(new RegExp(`HTTP/(?:1\\.1|2) ${status} `));
expect(body).toMatch(regex);
};
}
public writeBuildFile(pr: number, sha: string, relFilePath: string, content: string, isPublic = true,
legacy = false): void {
const shaDir = this.getShaDir(this.getPrDir(pr, isPublic), sha, legacy);
const absFilePath = path.join(shaDir, relFilePath);
this.writeFile(absFilePath, {content}, true);
}
public writeFile(filePath: string, {content, size}: FileSpecs, force = false): void {
if (!force && fs.existsSync(filePath)) {
throw new Error(`Refusing to overwrite existing file '${filePath}'.`);
}
let cleanUpTarget = filePath;
while (!fs.existsSync(path.dirname(cleanUpTarget))) {
cleanUpTarget = path.dirname(cleanUpTarget);
}
shell.mkdir('-p', path.dirname(filePath));
if (size) {
// Create a file of the specified size.
cp.execSync(`fallocate -l ${size} ${filePath}`);
} else {
// Create a file with the specified content.
fs.writeFileSync(filePath, content || '');
}
shell.exec(`chown ${AIO_WWW_USER} ${filePath}`);
}
// Methods - Protected
protected createCleanUpFn(fn: () => void): CleanUpFn {
const cleanUpFn = () => {
const idx = this.cleanUpFns.indexOf(cleanUpFn);
if (idx !== -1) {
this.cleanUpFns.splice(idx, 1);
fn();
}
};
this.cleanUpFns.push(cleanUpFn);
return cleanUpFn;
}
}
interface DefaultCurlOptions {
defaultMethod?: CurlOptions['method'];
defaultOptions?: CurlOptions['options'];
defaultHeaders?: CurlOptions['headers'];
defaultData?: CurlOptions['data'];
defaultExtraPath?: CurlOptions['extraPath'];
}
interface CurlOptions {
method?: string;
options?: string;
headers?: string[];
data?: any;
url?: string;
extraPath?: string;
}
export function makeCurl(baseUrl: string, {
defaultMethod = 'POST',
defaultOptions = '',
defaultHeaders = ['Content-Type: application/json'],
defaultData = {},
defaultExtraPath = '',
}: DefaultCurlOptions = {}) {
return function curl({
method = defaultMethod,
options = defaultOptions,
headers = defaultHeaders,
data = defaultData,
url = baseUrl,
extraPath = defaultExtraPath,
}: CurlOptions) {
const dataString = data ? JSON.stringify(data) : '';
const cmd = `curl -iLX ${method} ` +
`${options} ` +
headers.map(header => `--header "${header}" `).join('') +
`--data '${dataString}' ` +
`${url}${extraPath}`;
return helper.runCmd(cmd);
};
}
export interface PayloadData {
data: {
payload: {
build_num: number,
build_parameters: {
CIRCLE_JOB: string;
};
};
};
}
export function payload(buildNum: number): PayloadData {
return {
data: {
payload: {
build_num: buildNum,
build_parameters: { CIRCLE_JOB: 'aio_preview' },
},
},
};
}
// Exports
export const helper = new Helper();
@@ -0,0 +1,6 @@
// Imports
import {runTests} from '../common/run-tests';
// Run
const specFiles = [`${__dirname}/**/*.e2e.js`];
runTests(specFiles);
@@ -0,0 +1,7 @@
declare module jasmine {
interface Matchers {
toExistAsAFile(remove = true): boolean;
toExistAsABuild(remove = true): boolean;
toExistAsAnArtifact(remove = true): boolean;
}
}
@@ -0,0 +1,88 @@
import {sync as deleteEmpty} from 'delete-empty';
import {existsSync, unlinkSync} from 'fs';
import {join} from 'path';
import {AIO_DOWNLOADS_DIR} from '../common/constants';
import {computeShortSha} from '../common/utils';
import {SHA} from './constants';
import {helper} from './helper';
function checkFile(filePath: string, remove: boolean): boolean {
const exists = existsSync(filePath);
if (exists && remove) {
// if we expected the file to exist then we remove it to prevent leftover file errors
unlinkSync(filePath);
}
return exists;
}
function getArtifactPath(prNum: number, sha: string = SHA): string {
return `${AIO_DOWNLOADS_DIR}/${prNum}-${computeShortSha(sha)}-aio-snapshot.tgz`;
}
function checkFiles(prNum: number, isPublic: boolean, sha: string, isLegacy: boolean, remove: boolean) {
const files = ['/index.html', '/foo/bar.js'];
const prPath = helper.getPrDir(prNum, isPublic);
const shaPath = helper.getShaDir(prPath, sha, isLegacy);
const existingFiles: string[] = [];
const missingFiles: string[] = [];
files
.map(file => join(shaPath, file))
.forEach(file => (checkFile(file, remove) ? existingFiles : missingFiles).push(file));
deleteEmpty(prPath);
return { existingFiles, missingFiles };
}
class ToExistAsAFile implements jasmine.CustomMatcher {
public compare(actual: string, remove = true): jasmine.CustomMatcherResult {
const pass = checkFile(actual, remove);
return {
message: `Expected file at "${actual}" ${pass ? 'not' : ''} to exist`,
pass,
};
}
}
class ToExistAsAnArtifact implements jasmine.CustomMatcher {
public compare(actual: {prNum: number, sha?: string}, remove = true): jasmine.CustomMatcherResult {
const { prNum, sha = SHA } = actual;
const filePath = getArtifactPath(prNum, sha);
const pass = checkFile(filePath, remove);
return {
message: `Expected artifact "PR:${prNum}, SHA:${sha}, FILE:${filePath}" ${pass ? 'not' : '\b'} to exist`,
pass,
};
}
}
class ToExistAsABuild implements jasmine.CustomMatcher {
public compare(actual: {prNum: number, isPublic?: boolean, sha?: string, isLegacy?: boolean}, remove = true):
jasmine.CustomMatcherResult {
const {prNum, isPublic = true, sha = SHA, isLegacy = false} = actual;
const {missingFiles} = checkFiles(prNum, isPublic, sha, isLegacy, remove);
return {
message: `Expected files for build "PR:${prNum}, SHA:${sha}" to exist:\n` +
missingFiles.map(file => ` - ${file}`).join('\n'),
pass: missingFiles.length === 0,
};
}
public negativeCompare(actual: {prNum: number, isPublic?: boolean, sha?: string, isLegacy?: boolean}):
jasmine.CustomMatcherResult {
const {prNum, isPublic = true, sha = SHA, isLegacy = false} = actual;
const { existingFiles } = checkFiles(prNum, isPublic, sha, isLegacy, false);
return {
message: `Expected files for build "PR:${prNum}, SHA:${sha}" not to exist:\n` +
existingFiles.map(file => ` - ${file}`).join('\n'),
pass: existingFiles.length === 0,
};
}
}
export const customMatchers = {
toExistAsABuild: () => new ToExistAsABuild(),
toExistAsAFile: () => new ToExistAsAFile(),
toExistAsAnArtifact: () => new ToExistAsAnArtifact(),
};
@@ -0,0 +1,187 @@
/* tslint:disable:max-line-length */
import * as nock from 'nock';
import * as tar from 'tar-stream';
import {gzipSync} from 'zlib';
import {getEnvVar, Logger} from '../common/utils';
import {BuildNums, PipelineIds, PrNums, SHA} from './constants';
// We are using the `nock` library to fake responses from REST requests, when testing.
// This is necessary, because the test preview-server runs as a separate node process to
// the test harness, so we do not have direct access to the code (e.g. for mocking).
// (See also 'lib/verify-setup/start-test-preview-server.ts'.)
// Each of the potential requests to an external API (e.g. Github or CircleCI) are mocked
// below and return a suitable response. This is quite complicated to setup since the
// response from, say, CircleCI will affect what request is made to, say, Github.
const logger = new Logger('mock-external-apis');
const AIO_CIRCLE_CI_TOKEN = getEnvVar('AIO_CIRCLE_CI_TOKEN');
const AIO_GITHUB_TOKEN = getEnvVar('AIO_GITHUB_TOKEN');
const AIO_ARTIFACT_PATH = getEnvVar('AIO_ARTIFACT_PATH');
const AIO_GITHUB_ORGANIZATION = getEnvVar('AIO_GITHUB_ORGANIZATION');
const AIO_GITHUB_REPO = getEnvVar('AIO_GITHUB_REPO');
const AIO_TRUSTED_PR_LABEL = getEnvVar('AIO_TRUSTED_PR_LABEL');
const AIO_GITHUB_TEAM_SLUGS = getEnvVar('AIO_GITHUB_TEAM_SLUGS').split(',');
const ACTIVE_TRUSTED_USER = 'active-trusted-user';
const INACTIVE_TRUSTED_USER = 'inactive-trusted-user';
const UNTRUSTED_USER = 'untrusted-user';
const BASIC_BUILD_INFO = {
name: 'test_job',
organization: {name: AIO_GITHUB_ORGANIZATION},
project: {name: AIO_GITHUB_REPO},
pipeline: {id: PipelineIds.PIPELINE_INFO_OK},
};
const BASIC_PIPELINE_INFO = {
id: PipelineIds.PIPELINE_INFO_OK,
vcs: {
review_id: `${PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER}`,
revision: SHA,
},
};
const ISSUE_INFO_TRUSTED_LABEL = { labels: [{ name: AIO_TRUSTED_PR_LABEL }], user: { login: UNTRUSTED_USER } };
const ISSUE_INFO_ACTIVE_TRUSTED_USER = { labels: [], user: { login: ACTIVE_TRUSTED_USER } };
const ISSUE_INFO_INACTIVE_TRUSTED_USER = { labels: [], user: { login: INACTIVE_TRUSTED_USER } };
const ISSUE_INFO_UNTRUSTED = { labels: [], user: { login: UNTRUSTED_USER } };
const ACTIVE_STATE = { state: 'active' };
const INACTIVE_STATE = { state: 'inactive' };
const TEST_TEAM_INFO = AIO_GITHUB_TEAM_SLUGS.map((slug, index) => ({ slug, id: index }));
const CIRCLE_CI_API_HOST = 'https://circleci.com';
const ARTIFACT_1 = { path: 'artifact-1', url: `${CIRCLE_CI_API_HOST}/artifacts/artifact-1`, _urlPath: '/artifacts/artifact-1' };
const ARTIFACT_2 = { path: 'artifact-2', url: `${CIRCLE_CI_API_HOST}/artifacts/artifact-2`, _urlPath: '/artifacts/artifact-2' };
const ARTIFACT_3 = { path: 'artifact-3', url: `${CIRCLE_CI_API_HOST}/artifacts/artifact-3`, _urlPath: '/artifacts/artifact-3' };
const ARTIFACT_ERROR = { path: AIO_ARTIFACT_PATH, url: `${CIRCLE_CI_API_HOST}/artifacts/error`, _urlPath: '/artifacts/error' };
const ARTIFACT_404 = { path: AIO_ARTIFACT_PATH, url: `${CIRCLE_CI_API_HOST}/artifacts/404`, _urlPath: '/artifacts/404' };
const ARTIFACT_VALID_TRUSTED_USER = { path: AIO_ARTIFACT_PATH, url: `${CIRCLE_CI_API_HOST}/artifacts/valid/user`, _urlPath: '/artifacts/valid/user' };
const ARTIFACT_VALID_TRUSTED_LABEL = { path: AIO_ARTIFACT_PATH, url: `${CIRCLE_CI_API_HOST}/artifacts/valid/label`, _urlPath: '/artifacts/valid/label' };
const ARTIFACT_VALID_UNTRUSTED = { path: AIO_ARTIFACT_PATH, url: `${CIRCLE_CI_API_HOST}/artifacts/valid/untrusted`, _urlPath: '/artifacts/valid/untrusted' };
const CIRCLE_CI_BUILD_INFO_URL = `/api/v2/project/gh/${AIO_GITHUB_ORGANIZATION}/${AIO_GITHUB_REPO}`;
const CIRCLE_CI_PIPELINE_INFO_URL = '/api/v2/pipeline';
const buildInfoUrl = (buildNum: number) => `${CIRCLE_CI_BUILD_INFO_URL}/job/${buildNum}`;
const buildArtifactsUrl = (buildNum: number) => `${CIRCLE_CI_BUILD_INFO_URL}/${buildNum}/artifacts`;
const pipelineInfoUrl = (pipelineId: string) => `${CIRCLE_CI_PIPELINE_INFO_URL}/${pipelineId}`;
const buildInfo = (pipelineId: string) => ({ ...BASIC_BUILD_INFO, pipeline: { id: pipelineId } });
const GITHUB_API_HOST = 'https://api.github.com';
const GITHUB_ISSUES_URL = `/repos/${AIO_GITHUB_ORGANIZATION}/${AIO_GITHUB_REPO}/issues`;
const GITHUB_PULLS_URL = `/repos/${AIO_GITHUB_ORGANIZATION}/${AIO_GITHUB_REPO}/pulls`;
const GITHUB_TEAMS_URL = `/orgs/${AIO_GITHUB_ORGANIZATION}/teams`;
const getIssueUrl = (prNum: number) => `${GITHUB_ISSUES_URL}/${prNum}`;
const getFilesUrl = (prNum: number, pageNum = 1) => `${GITHUB_PULLS_URL}/${prNum}/files?page=${pageNum}&per_page=100`;
const getCommentUrl = (prNum: number) => `${getIssueUrl(prNum)}/comments`;
const getTeamMembershipUrl = (teamId: number, username: string) => `/teams/${teamId}/memberships/${username}`;
const setUpCircleCiApiForBuild =
(buildNum: number, pipelineId: string, prNum = PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER, sha = SHA) => {
circleCiApi.get(buildInfoUrl(buildNum)).reply(200, buildInfo(pipelineId));
circleCiApi.get(pipelineInfoUrl(pipelineId)).reply(200, {
id: pipelineId,
vcs: { review_id: `${prNum}`, revision: sha },
});
};
const createArchive = (buildNum: number, prNum: number, sha: string) => {
logger.log('createArchive', buildNum, prNum, sha);
const pack = tar.pack();
pack.entry({name: 'index.html'}, `BUILD: ${buildNum} | PR: ${prNum} | SHA: ${sha} | File: /index.html`);
pack.entry({name: 'foo/bar.js'}, `BUILD: ${buildNum} | PR: ${prNum} | SHA: ${sha} | File: /foo/bar.js`);
pack.finalize();
const zip = gzipSync(pack.read());
return zip;
};
// Create request scopes
const circleCiApi = nock(CIRCLE_CI_API_HOST).persist().matchHeader('Circle-Token', AIO_CIRCLE_CI_TOKEN);
const githubApi = nock(GITHUB_API_HOST).persist().matchHeader('Authorization', `token ${AIO_GITHUB_TOKEN}`);
//////////////////////////////
// GENERAL responses
githubApi.get(GITHUB_TEAMS_URL + '?page=1&per_page=100').reply(200, TEST_TEAM_INFO);
githubApi.post(getCommentUrl(PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER)).reply(200);
circleCiApi.get(pipelineInfoUrl(PipelineIds.PIPELINE_INFO_OK)).reply(200, BASIC_PIPELINE_INFO);
// BUILD INFO errors
circleCiApi.get(buildInfoUrl(BuildNums.BUILD_INFO_ERROR)).replyWithError('BUILD_INFO_ERROR');
circleCiApi.get(buildInfoUrl(BuildNums.BUILD_INFO_404)).reply(404, 'BUILD_INFO_404');
circleCiApi.get(buildInfoUrl(BuildNums.BUILD_INFO_INVALID_GH_ORG)).reply(200, { ...BASIC_BUILD_INFO, organization: { name: 'bad' } });
circleCiApi.get(buildInfoUrl(BuildNums.BUILD_INFO_INVALID_GH_REPO)).reply(200, { ...BASIC_BUILD_INFO, project: { name: 'bad' } });
// PIPELINE INFO errors
circleCiApi.get(buildInfoUrl(BuildNums.PIPELINE_INFO_ERROR)).reply(200, { ...BASIC_BUILD_INFO, pipeline: { id: PipelineIds.PIPELINE_INFO_ERROR } });
circleCiApi.get(pipelineInfoUrl(PipelineIds.PIPELINE_INFO_ERROR)).replyWithError('PIPELINE_INFO_ERROR');
circleCiApi.get(buildInfoUrl(BuildNums.PIPELINE_INFO_404)).reply(200, { ...BASIC_BUILD_INFO, pipeline: { id: PipelineIds.PIPELINE_INFO_404 } });
circleCiApi.get(pipelineInfoUrl(PipelineIds.PIPELINE_INFO_ERROR)).reply(404, 'PIPELINE_INFO_404');
// CHANGED FILE errors
setUpCircleCiApiForBuild(BuildNums.CHANGED_FILES_ERROR, PipelineIds.CHANGED_FILES_ERROR, PrNums.CHANGED_FILES_ERROR);
githubApi.get(getFilesUrl(PrNums.CHANGED_FILES_ERROR)).replyWithError('CHANGED_FILES_ERROR');
setUpCircleCiApiForBuild(BuildNums.CHANGED_FILES_404, PipelineIds.CHANGED_FILES_404, PrNums.CHANGED_FILES_404);
githubApi.get(getFilesUrl(PrNums.CHANGED_FILES_404)).reply(404, 'CHANGED_FILES_404');
setUpCircleCiApiForBuild(BuildNums.CHANGED_FILES_NONE, PipelineIds.CHANGED_FILES_NONE, PrNums.CHANGED_FILES_NONE);
githubApi.get(getFilesUrl(PrNums.CHANGED_FILES_NONE)).reply(200, []);
// ARTIFACT URL errors
circleCiApi.get(buildInfoUrl(BuildNums.BUILD_ARTIFACTS_ERROR)).reply(200, BASIC_BUILD_INFO);
circleCiApi.get(buildArtifactsUrl(BuildNums.BUILD_ARTIFACTS_ERROR)).replyWithError('BUILD_ARTIFACTS_ERROR');
circleCiApi.get(buildInfoUrl(BuildNums.BUILD_ARTIFACTS_404)).reply(200, BASIC_BUILD_INFO);
circleCiApi.get(buildArtifactsUrl(BuildNums.BUILD_ARTIFACTS_404)).reply(404, 'BUILD_ARTIFACTS_ERROR');
circleCiApi.get(buildInfoUrl(BuildNums.BUILD_ARTIFACTS_EMPTY)).reply(200, BASIC_BUILD_INFO);
circleCiApi.get(buildArtifactsUrl(BuildNums.BUILD_ARTIFACTS_EMPTY)).reply(200, { items: [] });
circleCiApi.get(buildInfoUrl(BuildNums.BUILD_ARTIFACTS_MISSING)).reply(200, BASIC_BUILD_INFO);
circleCiApi.get(buildArtifactsUrl(BuildNums.BUILD_ARTIFACTS_MISSING)).reply(200, { items: [ARTIFACT_1, ARTIFACT_2, ARTIFACT_3] });
// ARTIFACT DOWNLOAD errors
circleCiApi.get(buildInfoUrl(BuildNums.DOWNLOAD_ARTIFACT_ERROR)).reply(200, BASIC_BUILD_INFO);
circleCiApi.get(buildArtifactsUrl(BuildNums.DOWNLOAD_ARTIFACT_ERROR)).reply(200, { items: [ARTIFACT_ERROR] });
circleCiApi.get(ARTIFACT_ERROR._urlPath).replyWithError(ARTIFACT_ERROR._urlPath);
circleCiApi.get(buildInfoUrl(BuildNums.DOWNLOAD_ARTIFACT_404)).reply(200, BASIC_BUILD_INFO);
circleCiApi.get(buildArtifactsUrl(BuildNums.DOWNLOAD_ARTIFACT_404)).reply(200, [ARTIFACT_404]);
circleCiApi.get(ARTIFACT_ERROR._urlPath).reply(404, ARTIFACT_ERROR._urlPath);
// TRUST CHECK errors
setUpCircleCiApiForBuild(BuildNums.TRUST_CHECK_ERROR, PipelineIds.TRUST_CHECK_ERROR, PrNums.TRUST_CHECK_ERROR);
githubApi.get(getFilesUrl(PrNums.TRUST_CHECK_ERROR)).reply(200, [{ filename: 'aio/a' }]);
circleCiApi.get(buildArtifactsUrl(BuildNums.TRUST_CHECK_ERROR)).reply(200, { items: [ARTIFACT_VALID_TRUSTED_USER] });
githubApi.get(getIssueUrl(PrNums.TRUST_CHECK_ERROR)).replyWithError('TRUST_CHECK_ERROR');
// ACTIVE TRUSTED USER response
circleCiApi.get(buildInfoUrl(BuildNums.TRUST_CHECK_ACTIVE_TRUSTED_USER)).reply(200, BASIC_BUILD_INFO);
githubApi.get(getFilesUrl(PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER)).reply(200, [{ filename: 'aio/a' }]);
circleCiApi.get(buildArtifactsUrl(BuildNums.TRUST_CHECK_ACTIVE_TRUSTED_USER)).reply(200, { items: [ARTIFACT_VALID_TRUSTED_USER] });
circleCiApi.get(ARTIFACT_VALID_TRUSTED_USER._urlPath).reply(200, createArchive(BuildNums.TRUST_CHECK_ACTIVE_TRUSTED_USER, PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER, SHA));
githubApi.get(getIssueUrl(PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER)).reply(200, ISSUE_INFO_ACTIVE_TRUSTED_USER);
githubApi.get(getTeamMembershipUrl(0, ACTIVE_TRUSTED_USER)).reply(200, ACTIVE_STATE);
// TRUSTED LABEL response
circleCiApi.get(buildInfoUrl(BuildNums.TRUST_CHECK_TRUSTED_LABEL)).reply(200, BASIC_BUILD_INFO);
githubApi.get(getFilesUrl(PrNums.TRUST_CHECK_TRUSTED_LABEL)).reply(200, [{ filename: 'aio/a' }]);
circleCiApi.get(buildArtifactsUrl(BuildNums.TRUST_CHECK_TRUSTED_LABEL)).reply(200, { items: [ARTIFACT_VALID_TRUSTED_LABEL] });
circleCiApi.get(ARTIFACT_VALID_TRUSTED_LABEL._urlPath).reply(200, createArchive(BuildNums.TRUST_CHECK_TRUSTED_LABEL, PrNums.TRUST_CHECK_TRUSTED_LABEL, SHA));
githubApi.get(getIssueUrl(PrNums.TRUST_CHECK_TRUSTED_LABEL)).reply(200, ISSUE_INFO_TRUSTED_LABEL);
githubApi.get(getTeamMembershipUrl(0, ACTIVE_TRUSTED_USER)).reply(200, ACTIVE_STATE);
// INACTIVE TRUSTED USER response
circleCiApi.get(buildInfoUrl(BuildNums.TRUST_CHECK_INACTIVE_TRUSTED_USER)).reply(200, BASIC_BUILD_INFO);
githubApi.get(getFilesUrl(PrNums.TRUST_CHECK_INACTIVE_TRUSTED_USER)).reply(200, [{ filename: 'aio/a' }]);
circleCiApi.get(buildArtifactsUrl(BuildNums.TRUST_CHECK_INACTIVE_TRUSTED_USER)).reply(200, { items: [ARTIFACT_VALID_TRUSTED_USER] });
githubApi.get(getIssueUrl(PrNums.TRUST_CHECK_INACTIVE_TRUSTED_USER)).reply(200, ISSUE_INFO_INACTIVE_TRUSTED_USER);
githubApi.get(getTeamMembershipUrl(0, INACTIVE_TRUSTED_USER)).reply(200, INACTIVE_STATE);
// UNTRUSTED reponse
setUpCircleCiApiForBuild(BuildNums.TRUST_CHECK_UNTRUSTED, PipelineIds.TRUST_CHECK_UNTRUSTED, PrNums.TRUST_CHECK_UNTRUSTED);
githubApi.get(getFilesUrl(PrNums.TRUST_CHECK_UNTRUSTED)).reply(200, [{ filename: 'aio/a' }]);
circleCiApi.get(buildArtifactsUrl(BuildNums.TRUST_CHECK_UNTRUSTED)).reply(200, { items: [ARTIFACT_VALID_UNTRUSTED] });
circleCiApi.get(ARTIFACT_VALID_UNTRUSTED._urlPath).reply(200, createArchive(BuildNums.TRUST_CHECK_UNTRUSTED, PrNums.TRUST_CHECK_UNTRUSTED, SHA));
githubApi.get(getIssueUrl(PrNums.TRUST_CHECK_UNTRUSTED)).reply(200, ISSUE_INFO_UNTRUSTED);
githubApi.get(getTeamMembershipUrl(0, UNTRUSTED_USER)).reply(404);
@@ -0,0 +1,395 @@
// Imports
import * as path from 'path';
import {rm} from 'shelljs';
import {AIO_BUILDS_DIR, AIO_NGINX_HOSTNAME, AIO_NGINX_PORT_HTTP, AIO_NGINX_PORT_HTTPS} from '../common/env-variables';
import {computeShortSha} from '../common/utils';
import {PrNums} from './constants';
import {helper as h} from './helper';
import {customMatchers} from './jasmine-custom-matchers';
// Tests
describe(`nginx`, () => {
beforeEach(() => jasmine.DEFAULT_TIMEOUT_INTERVAL = 5000);
beforeEach(() => jasmine.addMatchers(customMatchers));
afterEach(() => h.cleanUp());
it('should redirect HTTP to HTTPS', async () => {
const httpHost = `${AIO_NGINX_HOSTNAME}:${AIO_NGINX_PORT_HTTP}`;
const httpsHost = `${AIO_NGINX_HOSTNAME}:${AIO_NGINX_PORT_HTTPS}`;
const urlMap = {
[`http://${httpHost}/`]: `https://${httpsHost}/`,
[`http://${httpHost}/foo`]: `https://${httpsHost}/foo`,
[`http://foo.${httpHost}/`]: `https://foo.${httpsHost}/`,
};
const verifyRedirection = async (fromUrl: string, toUrl: string) => {
const result = await h.runCmd(`curl -i ${fromUrl}`);
h.verifyResponse(307)(result);
const headers = result.stdout.split(/(?:\r?\n){2,}/)[0];
expect(headers).toContain(`Location: ${toUrl}`);
};
await Promise.all(Object.entries(urlMap).map(urls => verifyRedirection(...urls)));
});
h.runForAllSupportedSchemes((scheme, port) => describe(`(on ${scheme.toUpperCase()})`, () => {
const hostname = AIO_NGINX_HOSTNAME;
const host = `${hostname}:${port}`;
const pr = 9;
const sha9 = '9'.repeat(40);
const sha0 = '0'.repeat(40);
const shortSha9 = computeShortSha(sha9);
const shortSha0 = computeShortSha(sha0);
describe(`pr<pr>-<sha>.${host}/*`, () => {
describe('(for public builds)', () => {
beforeEach(() => {
h.createDummyBuild(pr, sha9);
h.createDummyBuild(pr, sha0);
});
afterEach(() => {
expect({ prNum: pr, sha: sha9 }).toExistAsABuild();
expect({ prNum: pr, sha: sha0 }).toExistAsABuild();
});
it('should return /index.html', async () => {
const origin = `${scheme}://pr${pr}-${shortSha9}.${host}`;
const bodyRegex = new RegExp(`^PR: ${pr} | SHA: ${sha9} | File: /index\\.html$`);
await Promise.all([
h.runCmd(`curl -iL ${origin}/index.html`).then(h.verifyResponse(200, bodyRegex)),
h.runCmd(`curl -iL ${origin}/`).then(h.verifyResponse(200, bodyRegex)),
h.runCmd(`curl -iL ${origin}`).then(h.verifyResponse(200, bodyRegex)),
]);
});
it('should return /index.html (for legacy builds)', async () => {
const origin = `${scheme}://pr${pr}-${sha9}.${host}`;
const bodyRegex = new RegExp(`^PR: ${pr} | SHA: ${sha9} | File: /index\\.html$`);
h.createDummyBuild(pr, sha9, true, false, true);
await Promise.all([
h.runCmd(`curl -iL ${origin}/index.html`).then(h.verifyResponse(200, bodyRegex)),
h.runCmd(`curl -iL ${origin}/`).then(h.verifyResponse(200, bodyRegex)),
h.runCmd(`curl -iL ${origin}`).then(h.verifyResponse(200, bodyRegex)),
]);
expect({ prNum: pr, sha: sha9, isLegacy: true }).toExistAsABuild();
});
it('should return /foo/bar.js', async () => {
const bodyRegex = new RegExp(`^PR: ${pr} | SHA: ${sha9} | File: /foo/bar\\.js$`);
await h.runCmd(`curl -iL ${scheme}://pr${pr}-${shortSha9}.${host}/foo/bar.js`).
then(h.verifyResponse(200, bodyRegex));
});
it('should return /foo/bar.js (for legacy builds)', async () => {
const origin = `${scheme}://pr${pr}-${sha9}.${host}`;
const bodyRegex = new RegExp(`^PR: ${pr} | SHA: ${sha9} | File: /foo/bar\\.js$`);
h.createDummyBuild(pr, sha9, true, false, true);
await h.runCmd(`curl -iL ${origin}/foo/bar.js`).then(h.verifyResponse(200, bodyRegex));
expect({ prNum: pr, sha: sha9, isLegacy: true }).toExistAsABuild();
});
it('should respond with 403 for directories', async () => {
await Promise.all([
h.runCmd(`curl -iL ${scheme}://pr${pr}-${shortSha9}.${host}/foo/`).then(h.verifyResponse(403)),
h.runCmd(`curl -iL ${scheme}://pr${pr}-${shortSha9}.${host}/foo`).then(h.verifyResponse(403)),
]);
});
it('should respond with 404 for unknown paths to files', async () => {
await h.runCmd(`curl -iL ${scheme}://pr${pr}-${shortSha9}.${host}/foo/baz.css`).
then(h.verifyResponse(404));
});
it('should rewrite to \'index.html\' for unknown paths that don\'t look like files', async () => {
const origin = `${scheme}://pr${pr}-${shortSha9}.${host}`;
const bodyRegex = new RegExp(`^PR: ${pr} | SHA: ${sha9} | File: /index\\.html$`);
await Promise.all([
h.runCmd(`curl -iL ${origin}/foo/baz`).then(h.verifyResponse(200, bodyRegex)),
h.runCmd(`curl -iL ${origin}/foo/baz/`).then(h.verifyResponse(200, bodyRegex)),
]);
});
it('should respond with 404 for unknown PRs/SHAs', async () => {
const otherPr = 54321;
const otherShortSha = computeShortSha('8'.repeat(40));
await Promise.all([
h.runCmd(`curl -iL ${scheme}://pr${pr}9-${shortSha9}.${host}`).then(h.verifyResponse(404)),
h.runCmd(`curl -iL ${scheme}://pr${otherPr}-${shortSha9}.${host}`).then(h.verifyResponse(404)),
h.runCmd(`curl -iL ${scheme}://pr${pr}-${shortSha9}9.${host}`).then(h.verifyResponse(404)),
h.runCmd(`curl -iL ${scheme}://pr${pr}-${otherShortSha}.${host}`).then(h.verifyResponse(404)),
]);
});
it('should respond with 404 if the subdomain format is wrong', async () => {
await Promise.all([
h.runCmd(`curl -iL ${scheme}://xpr${pr}-${shortSha9}.${host}`).then(h.verifyResponse(404)),
h.runCmd(`curl -iL ${scheme}://prx${pr}-${shortSha9}.${host}`).then(h.verifyResponse(404)),
h.runCmd(`curl -iL ${scheme}://xx${pr}-${shortSha9}.${host}`).then(h.verifyResponse(404)),
h.runCmd(`curl -iL ${scheme}://p${pr}-${shortSha9}.${host}`).then(h.verifyResponse(404)),
h.runCmd(`curl -iL ${scheme}://r${pr}-${shortSha9}.${host}`).then(h.verifyResponse(404)),
h.runCmd(`curl -iL ${scheme}://${pr}-${shortSha9}.${host}`).then(h.verifyResponse(404)),
h.runCmd(`curl -iL ${scheme}://pr${pr}${shortSha9}.${host}`).then(h.verifyResponse(404)),
h.runCmd(`curl -iL ${scheme}://pr${pr}_${shortSha9}.${host}`).then(h.verifyResponse(404)),
]);
});
it('should reject PRs with leading zeros', async () => {
await h.runCmd(`curl -iL ${scheme}://pr0${pr}-${shortSha9}.${host}`).
then(h.verifyResponse(404));
});
it('should accept SHAs with leading zeros (but not trim the zeros)', async () => {
const bodyRegex9 = new RegExp(`^PR: ${pr} | SHA: ${sha9} | File: /index\\.html$`);
const bodyRegex0 = new RegExp(`^PR: ${pr} | SHA: ${sha0} | File: /index\\.html$`);
await Promise.all([
h.runCmd(`curl -iL ${scheme}://pr${pr}-0${shortSha9}.${host}`).then(h.verifyResponse(404)),
h.runCmd(`curl -iL ${scheme}://pr${pr}-${shortSha9}.${host}`).then(h.verifyResponse(200, bodyRegex9)),
h.runCmd(`curl -iL ${scheme}://pr${pr}-${shortSha0}.${host}`).then(h.verifyResponse(200, bodyRegex0)),
]);
});
});
describe('(for hidden builds)', () => {
it('should respond with 404 for any file or directory', async () => {
const origin = `${scheme}://pr${pr}-${shortSha9}.${host}`;
const assert404 = h.verifyResponse(404);
h.createDummyBuild(pr, sha9, false);
await Promise.all([
h.runCmd(`curl -iL ${origin}/index.html`).then(assert404),
h.runCmd(`curl -iL ${origin}/`).then(assert404),
h.runCmd(`curl -iL ${origin}`).then(assert404),
h.runCmd(`curl -iL ${origin}/foo/bar.js`).then(assert404),
h.runCmd(`curl -iL ${origin}/foo/`).then(assert404),
h.runCmd(`curl -iL ${origin}/foo`).then(assert404),
]);
expect({ prNum: pr, sha: sha9, isPublic: false }).toExistAsABuild();
});
it('should respond with 404 for any file or directory (for legacy builds)', async () => {
const origin = `${scheme}://pr${pr}-${sha9}.${host}`;
const assert404 = h.verifyResponse(404);
h.createDummyBuild(pr, sha9, false, false, true);
await Promise.all([
h.runCmd(`curl -iL ${origin}/index.html`).then(assert404),
h.runCmd(`curl -iL ${origin}/`).then(assert404),
h.runCmd(`curl -iL ${origin}`).then(assert404),
h.runCmd(`curl -iL ${origin}/foo/bar.js`).then(assert404),
h.runCmd(`curl -iL ${origin}/foo/`).then(assert404),
h.runCmd(`curl -iL ${origin}/foo`).then(assert404),
]);
expect({ prNum: pr, sha: sha9, isPublic: false, isLegacy: true }).toExistAsABuild();
});
});
});
describe(`${host}/health-check`, () => {
it('should respond with 200', async () => {
await Promise.all([
h.runCmd(`curl -iL ${scheme}://${host}/health-check`).then(h.verifyResponse(200)),
h.runCmd(`curl -iL ${scheme}://${host}/health-check/`).then(h.verifyResponse(200)),
]);
});
it('should respond with 404 if the path does not match exactly', async () => {
await Promise.all([
h.runCmd(`curl -iL ${scheme}://${host}/health-check/foo`).then(h.verifyResponse(404)),
h.runCmd(`curl -iL ${scheme}://${host}/health-check-foo`).then(h.verifyResponse(404)),
h.runCmd(`curl -iL ${scheme}://${host}/health-checknfoo`).then(h.verifyResponse(404)),
h.runCmd(`curl -iL ${scheme}://${host}/foo/health-check`).then(h.verifyResponse(404)),
h.runCmd(`curl -iL ${scheme}://${host}/foo-health-check`).then(h.verifyResponse(404)),
h.runCmd(`curl -iL ${scheme}://${host}/foonhealth-check`).then(h.verifyResponse(404)),
]);
});
});
describe(`${host}/can-have-public-preview`, () => {
const baseUrl = `${scheme}://${host}/can-have-public-preview`;
it('should disallow non-GET requests', async () => {
await Promise.all([
h.runCmd(`curl -iLX POST ${baseUrl}/42`).then(h.verifyResponse(405)),
h.runCmd(`curl -iLX PUT ${baseUrl}/42`).then(h.verifyResponse(405)),
h.runCmd(`curl -iLX PATCH ${baseUrl}/42`).then(h.verifyResponse(405)),
h.runCmd(`curl -iLX DELETE ${baseUrl}/42`).then(h.verifyResponse(405)),
]);
});
it('should pass requests through to the preview server', async () => {
await h.runCmd(`curl -iLX GET ${baseUrl}/${PrNums.CHANGED_FILES_ERROR}`).
then(h.verifyResponse(500, /CHANGED_FILES_ERROR/));
});
it('should respond with 404 for unknown paths', async () => {
const cmdPrefix = `curl -iLX GET ${baseUrl}`;
await Promise.all([
h.runCmd(`${cmdPrefix}/foo/42`).then(h.verifyResponse(404)),
h.runCmd(`${cmdPrefix}-foo/42`).then(h.verifyResponse(404)),
h.runCmd(`${cmdPrefix}nfoo/42`).then(h.verifyResponse(404)),
h.runCmd(`${cmdPrefix}/42/foo`).then(h.verifyResponse(404)),
h.runCmd(`${cmdPrefix}/f00`).then(h.verifyResponse(404)),
h.runCmd(`${cmdPrefix}/`).then(h.verifyResponse(404)),
]);
});
});
describe(`${host}/circle-build`, () => {
it('should disallow non-POST requests', async () => {
const url = `${scheme}://${host}/circle-build`;
await Promise.all([
h.runCmd(`curl -iLX GET ${url}`).then(h.verifyResponse(405)),
h.runCmd(`curl -iLX PUT ${url}`).then(h.verifyResponse(405)),
h.runCmd(`curl -iLX PATCH ${url}`).then(h.verifyResponse(405)),
h.runCmd(`curl -iLX DELETE ${url}`).then(h.verifyResponse(405)),
]);
});
it('should pass requests through to the preview server', async () => {
await h.runCmd(`curl -iLX POST ${scheme}://${host}/circle-build`).
then(h.verifyResponse(400, /Incorrect body content. Expected JSON/));
});
it('should respond with 404 for unknown paths', async () => {
const cmdPrefix = `curl -iLX POST ${scheme}://${host}`;
await Promise.all([
h.runCmd(`${cmdPrefix}/foo/circle-build/`).then(h.verifyResponse(404)),
h.runCmd(`${cmdPrefix}/foo-circle-build/`).then(h.verifyResponse(404)),
h.runCmd(`${cmdPrefix}/fooncircle-build/`).then(h.verifyResponse(404)),
h.runCmd(`${cmdPrefix}/circle-build/foo/`).then(h.verifyResponse(404)),
h.runCmd(`${cmdPrefix}/circle-build-foo/`).then(h.verifyResponse(404)),
h.runCmd(`${cmdPrefix}/circle-buildnfoo/`).then(h.verifyResponse(404)),
h.runCmd(`${cmdPrefix}/circle-build/pr`).then(h.verifyResponse(404)),
h.runCmd(`${cmdPrefix}/circle-build/42`).then(h.verifyResponse(404)),
]);
});
});
describe(`${host}/pr-updated`, () => {
const url = `${scheme}://${host}/pr-updated`;
it('should disallow non-POST requests', async () => {
await Promise.all([
h.runCmd(`curl -iLX GET ${url}`).then(h.verifyResponse(405)),
h.runCmd(`curl -iLX PUT ${url}`).then(h.verifyResponse(405)),
h.runCmd(`curl -iLX PATCH ${url}`).then(h.verifyResponse(405)),
h.runCmd(`curl -iLX DELETE ${url}`).then(h.verifyResponse(405)),
]);
});
it('should pass requests through to the preview server', async () => {
await h.runCmd(`curl -iLX POST --header "Content-Type: application/json" ${url}`).
then(h.verifyResponse(400, /Missing or empty 'number' field/));
});
it('should respond with 404 for unknown paths', async () => {
const cmdPrefix = `curl -iLX POST ${scheme}://${host}`;
await Promise.all([
h.runCmd(`${cmdPrefix}/foo/pr-updated`).then(h.verifyResponse(404)),
h.runCmd(`${cmdPrefix}/foo-pr-updated`).then(h.verifyResponse(404)),
h.runCmd(`${cmdPrefix}/foonpr-updated`).then(h.verifyResponse(404)),
h.runCmd(`${cmdPrefix}/pr-updated/foo`).then(h.verifyResponse(404)),
h.runCmd(`${cmdPrefix}/pr-updated-foo`).then(h.verifyResponse(404)),
h.runCmd(`${cmdPrefix}/pr-updatednfoo`).then(h.verifyResponse(404)),
]);
});
});
describe(`${host}/*`, () => {
beforeEach(() => {
['index.html', 'foo.js', 'foo/index.html'].forEach(relFilePath => {
const absFilePath = path.join(AIO_BUILDS_DIR, relFilePath);
h.writeFile(absFilePath, {content: `File: /${relFilePath}`});
});
});
it('should respond with 404 for unknown URLs (even if the resource exists)', async () => {
await Promise.all([
h.runCmd(`curl -iL ${scheme}://${host}/index.html`).then(h.verifyResponse(404)),
h.runCmd(`curl -iL ${scheme}://${host}/`).then(h.verifyResponse(404)),
h.runCmd(`curl -iL ${scheme}://${host}`).then(h.verifyResponse(404)),
h.runCmd(`curl -iL ${scheme}://foo.${host}/index.html`).then(h.verifyResponse(404)),
h.runCmd(`curl -iL ${scheme}://foo.${host}/`).then(h.verifyResponse(404)),
h.runCmd(`curl -iL ${scheme}://foo.${host}`).then(h.verifyResponse(404)),
h.runCmd(`curl -iL ${scheme}://${host}/foo.js`).then(h.verifyResponse(404)),
h.runCmd(`curl -iL ${scheme}://${host}/foo/index.html`).then(h.verifyResponse(404)),
]);
});
afterEach(() => {
['index.html', 'foo.js', 'foo/index.html', 'foo'].forEach(relFilePath => {
const absFilePath = path.join(AIO_BUILDS_DIR, relFilePath);
rm('-r', absFilePath);
});
});
});
}));
});
@@ -0,0 +1,566 @@
// Imports
import * as fs from 'fs';
import {join} from 'path';
import {AIO_PREVIEW_SERVER_HOSTNAME, AIO_PREVIEW_SERVER_PORT, AIO_WWW_USER} from '../common/env-variables';
import {computeShortSha} from '../common/utils';
import {ALT_SHA, BuildNums, PrNums, SHA, SIMILAR_SHA} from './constants';
import {helper as h, makeCurl, payload} from './helper';
import {customMatchers} from './jasmine-custom-matchers';
// Tests
describe('preview-server', () => {
const hostname = AIO_PREVIEW_SERVER_HOSTNAME;
const port = AIO_PREVIEW_SERVER_PORT;
const host = `http://${hostname}:${port}`;
beforeEach(() => jasmine.DEFAULT_TIMEOUT_INTERVAL = 5000);
beforeEach(() => jasmine.addMatchers(customMatchers));
afterEach(() => h.cleanUp());
describe(`${host}/can-have-public-preview`, () => {
const curl = makeCurl(`${host}/can-have-public-preview`, {
defaultData: null,
defaultExtraPath: `/${PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER}`,
defaultHeaders: [],
defaultMethod: 'GET',
});
it('should disallow non-GET requests', async () => {
const bodyRegex = /^Unknown resource in request/;
await Promise.all([
curl({method: 'POST'}).then(h.verifyResponse(404, bodyRegex)),
curl({method: 'PUT'}).then(h.verifyResponse(404, bodyRegex)),
curl({method: 'PATCH'}).then(h.verifyResponse(404, bodyRegex)),
curl({method: 'DELETE'}).then(h.verifyResponse(404, bodyRegex)),
]);
});
it('should respond with 404 for unknown paths', async () => {
const bodyRegex = /^Unknown resource in request/;
await Promise.all([
curl({extraPath: `/foo/${PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER}`}).then(h.verifyResponse(404, bodyRegex)),
curl({extraPath: `-foo/${PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER}`}).then(h.verifyResponse(404, bodyRegex)),
curl({extraPath: `nfoo/${PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER}`}).then(h.verifyResponse(404, bodyRegex)),
curl({extraPath: `/${PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER}/foo`}).then(h.verifyResponse(404, bodyRegex)),
curl({extraPath: '/f00'}).then(h.verifyResponse(404, bodyRegex)),
curl({extraPath: '/'}).then(h.verifyResponse(404, bodyRegex)),
]);
});
it('should respond with 500 if checking for significant file changes fails', async () => {
await Promise.all([
curl({extraPath: `/${PrNums.CHANGED_FILES_404}`}).then(h.verifyResponse(500, /CHANGED_FILES_404/)),
curl({extraPath: `/${PrNums.CHANGED_FILES_ERROR}`}).then(h.verifyResponse(500, /CHANGED_FILES_ERROR/)),
]);
});
it('should respond with 200 (false) if no significant files were touched', async () => {
const expectedResponse = JSON.stringify({
canHavePublicPreview: false,
reason: 'No significant files touched.',
});
await curl({extraPath: `/${PrNums.CHANGED_FILES_NONE}`}).then(h.verifyResponse(200, expectedResponse));
});
it('should respond with 500 if checking "trusted" status fails', async () => {
await curl({extraPath: `/${PrNums.TRUST_CHECK_ERROR}`}).then(h.verifyResponse(500, 'TRUST_CHECK_ERROR'));
});
it('should respond with 200 (false) if the PR is not automatically verifiable as "trusted"', async () => {
const expectedResponse = JSON.stringify({
canHavePublicPreview: false,
reason: 'Not automatically verifiable as \\"trusted\\".',
});
await Promise.all([
curl({extraPath: `/${PrNums.TRUST_CHECK_INACTIVE_TRUSTED_USER}`}).then(h.verifyResponse(200, expectedResponse)),
curl({extraPath: `/${PrNums.TRUST_CHECK_UNTRUSTED}`}).then(h.verifyResponse(200, expectedResponse)),
]);
});
it('should respond with 200 (true) if the PR can have a public preview', async () => {
const expectedResponse = JSON.stringify({
canHavePublicPreview: true,
reason: null,
});
await Promise.all([
curl({extraPath: `/${PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER}`}).then(h.verifyResponse(200, expectedResponse)),
curl({extraPath: `/${PrNums.TRUST_CHECK_TRUSTED_LABEL}`}).then(h.verifyResponse(200, expectedResponse)),
]);
});
});
describe(`${host}/circle-build`, () => {
const curl = makeCurl(`${host}/circle-build`);
it('should disallow non-POST requests', async () => {
const bodyRegex = /^Unknown resource/;
await Promise.all([
curl({method: 'GET'}).then(h.verifyResponse(404, bodyRegex)),
curl({method: 'PUT'}).then(h.verifyResponse(404, bodyRegex)),
curl({method: 'PATCH'}).then(h.verifyResponse(404, bodyRegex)),
curl({method: 'DELETE'}).then(h.verifyResponse(404, bodyRegex)),
]);
});
it('should respond with 404 for unknown paths', async () => {
await Promise.all([
curl({url: `${host}/foo/circle-build`}).then(h.verifyResponse(404)),
curl({url: `${host}/foo-circle-build`}).then(h.verifyResponse(404)),
curl({url: `${host}/fooncircle-build`}).then(h.verifyResponse(404)),
curl({url: `${host}/circle-build/foo`}).then(h.verifyResponse(404)),
curl({url: `${host}/circle-build-foo`}).then(h.verifyResponse(404)),
curl({url: `${host}/circle-buildnfoo`}).then(h.verifyResponse(404)),
curl({url: `${host}/circle-build/pr`}).then(h.verifyResponse(404)),
curl({url: `${host}/circle-build42`}).then(h.verifyResponse(404)),
]);
});
it('should respond with 400 if the body is not valid', async () => {
await Promise.all([
curl({ data: '' }).then(h.verifyResponse(400)),
curl({ data: {} }).then(h.verifyResponse(400)),
curl({ data: { payload: {} } }).then(h.verifyResponse(400)),
curl({ data: { payload: { build_num: 1 } } }).then(h.verifyResponse(400)),
curl({ data: { payload: { build_num: 1, build_parameters: {} } } }).then(h.verifyResponse(400)),
curl(payload(0)).then(h.verifyResponse(400)),
curl(payload(-1)).then(h.verifyResponse(400)),
]);
});
it('should respond with 500 if any of the CircleCI API requests errors', async () => {
await curl(payload(BuildNums.BUILD_INFO_ERROR)).then(h.verifyResponse(500));
await curl(payload(BuildNums.BUILD_INFO_404)).then(h.verifyResponse(500));
await curl(payload(BuildNums.PIPELINE_INFO_ERROR)).then(h.verifyResponse(500));
await curl(payload(BuildNums.PIPELINE_INFO_404)).then(h.verifyResponse(500));
});
it('should respond with 500 if the github org from CircleCI does not match what is configured', async () => {
await curl(payload(BuildNums.BUILD_INFO_INVALID_GH_ORG)).then(h.verifyResponse(500));
});
it('should respond with 500 if the github repo from CircleCI does not match what is configured', async () => {
await curl(payload(BuildNums.BUILD_INFO_INVALID_GH_REPO)).then(h.verifyResponse(500));
});
it('should respond with 500 if the github files API errors', async () => {
await curl(payload(BuildNums.CHANGED_FILES_ERROR)).then(h.verifyResponse(500));
await curl(payload(BuildNums.CHANGED_FILES_404)).then(h.verifyResponse(500));
});
it('should respond with 204 if no significant files are changed by the PR', async () => {
await curl(payload(BuildNums.CHANGED_FILES_NONE)).then(h.verifyResponse(204));
});
it('should respond with 500 if the CircleCI artifact API fails', async () => {
await curl(payload(BuildNums.BUILD_ARTIFACTS_ERROR)).then(h.verifyResponse(500));
await curl(payload(BuildNums.BUILD_ARTIFACTS_404)).then(h.verifyResponse(500));
await curl(payload(BuildNums.BUILD_ARTIFACTS_EMPTY)).then(h.verifyResponse(500));
await curl(payload(BuildNums.BUILD_ARTIFACTS_MISSING)).then(h.verifyResponse(500));
});
it('should respond with 500 if fetching the artifact errors', async () => {
await curl(payload(BuildNums.DOWNLOAD_ARTIFACT_ERROR)).then(h.verifyResponse(500));
await curl(payload(BuildNums.DOWNLOAD_ARTIFACT_404)).then(h.verifyResponse(500));
});
it('should respond with 500 if the GH trusted API fails', async () => {
await curl(payload(BuildNums.TRUST_CHECK_ERROR)).then(h.verifyResponse(500));
expect({ prNum: PrNums.TRUST_CHECK_ERROR }).toExistAsAnArtifact();
});
it('should respond with 201 if a new public build is created', async () => {
await curl(payload(BuildNums.TRUST_CHECK_ACTIVE_TRUSTED_USER)).then(h.verifyResponse(201));
expect({ prNum: PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER }).toExistAsABuild();
});
it('should respond with 202 if a new private build is created', async () => {
await curl(payload(BuildNums.TRUST_CHECK_UNTRUSTED)).then(h.verifyResponse(202));
expect({ prNum: PrNums.TRUST_CHECK_UNTRUSTED, isPublic: false }).toExistAsABuild();
});
[true, false].forEach(isPublic => {
const build = isPublic ? BuildNums.TRUST_CHECK_ACTIVE_TRUSTED_USER : BuildNums.TRUST_CHECK_UNTRUSTED;
const prNum = isPublic ? PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER : PrNums.TRUST_CHECK_UNTRUSTED;
const label = isPublic ? 'public' : 'non-public';
const overwriteRe = RegExp(`^Request to overwrite existing ${label} directory`);
const statusCode = isPublic ? 201 : 202;
describe(`for ${label} builds`, () => {
it('should extract the contents of the build artifact', async () => {
await curl(payload(build))
.then(h.verifyResponse(statusCode));
expect(h.readBuildFile(prNum, SHA, 'index.html', isPublic))
.toContain(`PR: ${prNum} | SHA: ${SHA} | File: /index.html`);
expect(h.readBuildFile(prNum, SHA, 'foo/bar.js', isPublic))
.toContain(`PR: ${prNum} | SHA: ${SHA} | File: /foo/bar.js`);
expect({ prNum, isPublic }).toExistAsABuild();
});
it(`should create files/directories owned by '${AIO_WWW_USER}'`, async () => {
await curl(payload(build))
.then(h.verifyResponse(statusCode));
const shaDir = h.getShaDir(h.getPrDir(prNum, isPublic), SHA);
const { stdout: allFiles } = await h.runCmd(`find ${shaDir}`);
const { stdout: userFiles } = await h.runCmd(`find ${shaDir} -user ${AIO_WWW_USER}`);
expect(userFiles).toBe(allFiles);
expect(userFiles).toContain(shaDir);
expect(userFiles).toContain(join(shaDir, 'index.html'));
expect(userFiles).toContain(join(shaDir, 'foo', 'bar.js'));
expect({ prNum, isPublic }).toExistAsABuild();
});
it('should delete the build artifact file', async () => {
await curl(payload(build))
.then(h.verifyResponse(statusCode));
expect({ prNum, SHA }).not.toExistAsAnArtifact();
expect({ prNum, isPublic }).toExistAsABuild();
});
it('should make the build directory non-writable', async () => {
await curl(payload(build))
.then(h.verifyResponse(statusCode));
// See https://github.com/nodejs/node-v0.x-archive/issues/3045#issuecomment-4862588.
const isNotWritable = (fileOrDir: string) => {
const mode = fs.statSync(fileOrDir).mode;
// tslint:disable-next-line: no-bitwise
return !(mode & parseInt('222', 8));
};
const shaDir = h.getShaDir(h.getPrDir(prNum, isPublic), SHA);
expect(isNotWritable(shaDir)).toBe(true);
expect(isNotWritable(join(shaDir, 'index.html'))).toBe(true);
expect(isNotWritable(join(shaDir, 'foo', 'bar.js'))).toBe(true);
expect({ prNum, isPublic }).toExistAsABuild();
});
it('should ignore a legacy 40-chars long build directory (even if it starts with the same chars)',
async () => {
// It is possible that 40-chars long build directories exist, if they had been deployed
// before implementing the shorter build directory names. In that case, we don't want the
// second (shorter) name to be considered the same as the old one (even if they originate
// from the same SHA).
h.createDummyBuild(prNum, SHA, isPublic, false, true);
h.writeBuildFile(prNum, SHA, 'index.html', 'My content', isPublic, true);
expect(h.readBuildFile(prNum, SHA, 'index.html', isPublic, true)).toBe('My content');
await curl(payload(build))
.then(h.verifyResponse(statusCode));
expect(h.readBuildFile(prNum, SHA, 'index.html', isPublic, false)).toContain('index.html');
expect(h.readBuildFile(prNum, SHA, 'index.html', isPublic, true)).toBe('My content');
expect({ prNum, isPublic, sha: SHA, isLegacy: false }).toExistAsABuild();
expect({ prNum, isPublic, sha: SHA, isLegacy: true }).toExistAsABuild();
});
it(`should not overwrite existing builds`, async () => {
// setup a build already in place
h.createDummyBuild(prNum, SHA, isPublic);
// distinguish this build from the downloaded one
h.writeBuildFile(prNum, SHA, 'index.html', 'My content', isPublic);
await curl(payload(build)).then(h.verifyResponse(409, overwriteRe));
expect(h.readBuildFile(prNum, SHA, 'index.html', isPublic)).toBe('My content');
expect({ prNum, isPublic }).toExistAsABuild();
expect({ prNum }).toExistAsAnArtifact();
});
it(`should not overwrite existing builds (even if the SHA is different)`, async () => {
// Since only the first few characters of the SHA are used, it is possible for two different
// SHAs to correspond to the same directory. In that case, we don't want the second SHA to
// overwrite the first.
expect(SIMILAR_SHA).not.toEqual(SHA);
expect(computeShortSha(SIMILAR_SHA)).toEqual(computeShortSha(SHA));
h.createDummyBuild(prNum, SIMILAR_SHA, isPublic);
expect(h.readBuildFile(prNum, SIMILAR_SHA, 'index.html', isPublic)).toContain('index.html');
h.writeBuildFile(prNum, SIMILAR_SHA, 'index.html', 'My content', isPublic);
expect(h.readBuildFile(prNum, SIMILAR_SHA, 'index.html', isPublic)).toBe('My content');
await curl(payload(build)).then(h.verifyResponse(409, overwriteRe));
expect(h.readBuildFile(prNum, SIMILAR_SHA, 'index.html', isPublic)).toBe('My content');
expect({ prNum, isPublic, sha: SIMILAR_SHA }).toExistAsABuild();
expect({ prNum, sha: SIMILAR_SHA }).toExistAsAnArtifact();
});
it('should only delete the SHA directory on error (for existing PR)', async () => {
h.createDummyBuild(prNum, ALT_SHA, isPublic);
await curl(payload(BuildNums.TRUST_CHECK_ERROR)).then(h.verifyResponse(500));
expect({ prNum: PrNums.TRUST_CHECK_ERROR }).toExistAsAnArtifact();
expect({ prNum, isPublic, sha: SHA }).not.toExistAsABuild();
expect({ prNum, isPublic, sha: ALT_SHA }).toExistAsABuild();
});
describe('when the PR\'s visibility has changed', () => {
it('should update the PR\'s visibility', async () => {
h.createDummyBuild(prNum, ALT_SHA, !isPublic);
await curl(payload(build)).then(h.verifyResponse(statusCode));
expect({ prNum, isPublic }).toExistAsABuild();
expect({ prNum, isPublic, sha: ALT_SHA }).toExistAsABuild();
});
it('should not overwrite existing builds (but keep the updated visibility)', async () => {
h.createDummyBuild(prNum, SHA, !isPublic);
await curl(payload(build)).then(h.verifyResponse(409));
expect({ prNum, isPublic }).toExistAsABuild();
expect({ prNum, isPublic: !isPublic }).not.toExistAsABuild();
// since it errored we didn't clear up the downloaded artifact - perhaps we should?
expect({ prNum }).toExistAsAnArtifact();
});
it('should reject the request if it fails to update the PR\'s visibility', async () => {
// One way to cause an error is to have both a public and a hidden directory for the same PR.
h.createDummyBuild(prNum, ALT_SHA, isPublic);
h.createDummyBuild(prNum, ALT_SHA, !isPublic);
const errorRegex = new RegExp(`^Request to move '${h.getPrDir(prNum, !isPublic)}' ` +
`to existing directory '${h.getPrDir(prNum, isPublic)}'.`);
await curl(payload(build)).then(h.verifyResponse(409, errorRegex));
expect({ prNum, isPublic }).not.toExistAsABuild();
// The bad folders should have been deleted
expect({ prNum, sha: ALT_SHA, isPublic }).toExistAsABuild();
expect({ prNum, sha: ALT_SHA, isPublic: !isPublic }).toExistAsABuild();
// since it errored we didn't clear up the downloaded artifact - perhaps we should?
expect({ prNum }).toExistAsAnArtifact();
});
});
});
});
});
describe(`${host}/health-check`, () => {
it('should respond with 200', async () => {
await Promise.all([
h.runCmd(`curl -iL ${host}/health-check`).then(h.verifyResponse(200)),
h.runCmd(`curl -iL ${host}/health-check/`).then(h.verifyResponse(200)),
]);
});
it('should respond with 404 if the path does not match exactly', async () => {
await Promise.all([
h.runCmd(`curl -iL ${host}/health-check/foo`).then(h.verifyResponse(404)),
h.runCmd(`curl -iL ${host}/health-check-foo`).then(h.verifyResponse(404)),
h.runCmd(`curl -iL ${host}/health-checknfoo`).then(h.verifyResponse(404)),
h.runCmd(`curl -iL ${host}/foo/health-check`).then(h.verifyResponse(404)),
h.runCmd(`curl -iL ${host}/foo-health-check`).then(h.verifyResponse(404)),
h.runCmd(`curl -iL ${host}/foonhealth-check`).then(h.verifyResponse(404)),
]);
});
});
describe(`${host}/pr-updated`, () => {
const curl = makeCurl(`${host}/pr-updated`);
it('should disallow non-POST requests', async () => {
const bodyRegex = /^Unknown resource in request/;
await Promise.all([
curl({method: 'GET'}).then(h.verifyResponse(404, bodyRegex)),
curl({method: 'PUT'}).then(h.verifyResponse(404, bodyRegex)),
curl({method: 'PATCH'}).then(h.verifyResponse(404, bodyRegex)),
curl({method: 'DELETE'}).then(h.verifyResponse(404, bodyRegex)),
]);
});
it('should respond with 400 for requests without a payload', async () => {
const bodyRegex = /^Missing or empty 'number' field in request/;
await Promise.all([
curl({ data: '' }).then(h.verifyResponse(400, bodyRegex)),
curl({ data: {} }).then(h.verifyResponse(400, bodyRegex)),
]);
});
it('should respond with 400 for requests without a \'number\' field', async () => {
const bodyRegex = /^Missing or empty 'number' field in request/;
await Promise.all([
curl({ data: {} }).then(h.verifyResponse(400, bodyRegex)),
curl({ data: { number: null} }).then(h.verifyResponse(400, bodyRegex)),
]);
});
it('should reject requests for which checking the PR visibility fails', async () => {
await curl({ data: { number: PrNums.TRUST_CHECK_ERROR } }).then(h.verifyResponse(500, /TRUST_CHECK_ERROR/));
});
it('should respond with 404 for unknown paths', async () => {
const mockPayload = JSON.stringify({number: 1}); // MockExternalApiFlags.TRUST_CHECK_ACTIVE_TRUSTED_USER });
const cmdPrefix = `curl -iLX POST --data "${mockPayload}" ${host}`;
await Promise.all([
h.runCmd(`${cmdPrefix}/foo/pr-updated`).then(h.verifyResponse(404)),
h.runCmd(`${cmdPrefix}/foo-pr-updated`).then(h.verifyResponse(404)),
h.runCmd(`${cmdPrefix}/foonpr-updated`).then(h.verifyResponse(404)),
h.runCmd(`${cmdPrefix}/pr-updated/foo`).then(h.verifyResponse(404)),
h.runCmd(`${cmdPrefix}/pr-updated-foo`).then(h.verifyResponse(404)),
h.runCmd(`${cmdPrefix}/pr-updatednfoo`).then(h.verifyResponse(404)),
]);
});
it('should do nothing if PR\'s visibility is already up-to-date', async () => {
const publicPr = PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER;
const hiddenPr = PrNums.TRUST_CHECK_UNTRUSTED;
const checkVisibilities = (remove: boolean) => {
// Public build is already public.
expect({ prNum: publicPr, isPublic: false }).not.toExistAsABuild(remove);
expect({ prNum: publicPr, isPublic: true }).toExistAsABuild(remove);
// Hidden build is already hidden.
expect({ prNum: hiddenPr, isPublic: false }).toExistAsABuild(remove);
expect({ prNum: hiddenPr, isPublic: true }).not.toExistAsABuild(remove);
};
h.createDummyBuild(publicPr, SHA, true);
h.createDummyBuild(hiddenPr, SHA, false);
checkVisibilities(false);
await Promise.all([
curl({ data: {number: +publicPr, action: 'foo' } }).then(h.verifyResponse(200)),
curl({ data: {number: +hiddenPr, action: 'foo' } }).then(h.verifyResponse(200)),
]);
// Visibilities should not have changed, because the specified action could not have triggered a change.
checkVisibilities(true);
});
it('should do nothing if \'action\' implies no visibility change', async () => {
const publicPr = PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER;
const hiddenPr = PrNums.TRUST_CHECK_UNTRUSTED;
const checkVisibilities = (remove: boolean) => {
// Public build is hidden atm.
expect({ prNum: publicPr, isPublic: false }).toExistAsABuild(remove);
expect({ prNum: publicPr, isPublic: true }).not.toExistAsABuild(remove);
// Hidden build is public atm.
expect({ prNum: hiddenPr, isPublic: false }).not.toExistAsABuild(remove);
expect({ prNum: hiddenPr, isPublic: true }).toExistAsABuild(remove);
};
h.createDummyBuild(publicPr, SHA, false);
h.createDummyBuild(hiddenPr, SHA, true);
checkVisibilities(false);
await Promise.all([
curl({ data: {number: +publicPr, action: 'foo' } }).then(h.verifyResponse(200)),
curl({ data: {number: +hiddenPr, action: 'foo' } }).then(h.verifyResponse(200)),
]);
// Visibilities should not have changed, because the specified action could not have triggered a change.
checkVisibilities(true);
});
describe('when the visiblity has changed', () => {
const publicPr = PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER;
const hiddenPr = PrNums.TRUST_CHECK_UNTRUSTED;
beforeEach(() => {
// Create initial PR builds with opposite visibilities as the ones that will be reported:
// - The now public PR was previously hidden.
// - The now hidden PR was previously public.
h.createDummyBuild(publicPr, SHA, false);
h.createDummyBuild(hiddenPr, SHA, true);
expect({ prNum: publicPr, isPublic: false }).toExistAsABuild(false);
expect({ prNum: publicPr, isPublic: true }).not.toExistAsABuild(false);
expect({ prNum: hiddenPr, isPublic: false }).not.toExistAsABuild(false);
expect({ prNum: hiddenPr, isPublic: true }).toExistAsABuild(false);
});
afterEach(() => {
// Expect PRs' visibility to have been updated:
// - The public PR should be actually public (previously it was hidden).
// - The hidden PR should be actually hidden (previously it was public).
expect({ prNum: publicPr, isPublic: false }).not.toExistAsABuild();
expect({ prNum: publicPr, isPublic: true }).toExistAsABuild();
expect({ prNum: hiddenPr, isPublic: false }).toExistAsABuild();
expect({ prNum: hiddenPr, isPublic: true }).not.toExistAsABuild();
});
it('should update the PR\'s visibility (action: undefined)', async () => {
await Promise.all([
curl({ data: {number: +publicPr } }).then(h.verifyResponse(200)),
curl({ data: {number: +hiddenPr } }).then(h.verifyResponse(200)),
]);
});
it('should update the PR\'s visibility (action: labeled)', async () => {
await Promise.all([
curl({ data: {number: +publicPr, action: 'labeled' } }).then(h.verifyResponse(200)),
curl({ data: {number: +hiddenPr, action: 'labeled' } }).then(h.verifyResponse(200)),
]);
});
it('should update the PR\'s visibility (action: unlabeled)', async () => {
await Promise.all([
curl({ data: {number: +publicPr, action: 'unlabeled' } }).then(h.verifyResponse(200)),
curl({ data: {number: +hiddenPr, action: 'unlabeled' } }).then(h.verifyResponse(200)),
]);
});
});
});
describe(`${host}/*`, () => {
it('should respond with 404 for requests to unknown URLs', async () => {
const bodyRegex = /^Unknown resource/;
await Promise.all([
h.runCmd(`curl -iL ${host}/index.html`).then(h.verifyResponse(404, bodyRegex)),
h.runCmd(`curl -iL ${host}/`).then(h.verifyResponse(404, bodyRegex)),
h.runCmd(`curl -iL ${host}`).then(h.verifyResponse(404, bodyRegex)),
h.runCmd(`curl -iLX PUT ${host}`).then(h.verifyResponse(404, bodyRegex)),
h.runCmd(`curl -iLX POST ${host}`).then(h.verifyResponse(404, bodyRegex)),
h.runCmd(`curl -iLX PATCH ${host}`).then(h.verifyResponse(404, bodyRegex)),
h.runCmd(`curl -iLX DELETE ${host}`).then(h.verifyResponse(404, bodyRegex)),
]);
});
});
});
@@ -0,0 +1,269 @@
// Imports
import {AIO_NGINX_HOSTNAME} from '../common/env-variables';
import {computeShortSha} from '../common/utils';
import {ALT_SHA, BuildNums, PrNums, SHA} from './constants';
import {helper as h, makeCurl, payload} from './helper';
import {customMatchers} from './jasmine-custom-matchers';
// Tests
h.runForAllSupportedSchemes((scheme, port) => describe(`integration (on ${scheme.toUpperCase()})`, () => {
const hostname = AIO_NGINX_HOSTNAME;
const host = `${hostname}:${port}`;
const curlPrUpdated = makeCurl(`${scheme}://${host}/pr-updated`);
const getFile = (pr: number, sha: string, file: string) =>
h.runCmd(`curl -iL ${scheme}://pr${pr}-${computeShortSha(sha)}.${host}/${file}`);
const prUpdated = (prNum: number, action?: string) => curlPrUpdated({ data: { number: prNum, action } });
const circleBuild = makeCurl(`${scheme}://${host}/circle-build`);
beforeEach(() => {
jasmine.DEFAULT_TIMEOUT_INTERVAL = 5000;
jasmine.addMatchers(customMatchers);
});
afterEach(() => h.cleanUp());
describe('for a new/non-existing PR', () => {
it('should be able to create and serve a public preview', async () => {
const BUILD = BuildNums.TRUST_CHECK_ACTIVE_TRUSTED_USER;
const PR = PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER;
const regexPrefix = `^BUILD: ${BUILD} \\| PR: ${PR} \\| SHA: ${SHA} \\| File:`;
const idxContentRegex = new RegExp(`${regexPrefix} \\/index\\.html$`);
const barContentRegex = new RegExp(`${regexPrefix} \\/foo\\/bar\\.js$`);
await circleBuild(payload(BUILD)).then(h.verifyResponse(201));
await Promise.all([
getFile(PR, SHA, 'index.html').then(h.verifyResponse(200, idxContentRegex)),
getFile(PR, SHA, 'foo/bar.js').then(h.verifyResponse(200, barContentRegex)),
]);
expect({ prNum: PR }).toExistAsABuild();
expect({ prNum: PR, isPublic: false }).not.toExistAsABuild();
});
it('should be able to create but not serve a hidden preview', async () => {
const BUILD = BuildNums.TRUST_CHECK_UNTRUSTED;
const PR = PrNums.TRUST_CHECK_UNTRUSTED;
await circleBuild(payload(BUILD)).then(h.verifyResponse(202));
await Promise.all([
getFile(PR, SHA, 'index.html').then(h.verifyResponse(404)),
getFile(PR, SHA, 'foo/bar.js').then(h.verifyResponse(404)),
]);
expect({ prNum: PR }).not.toExistAsABuild();
expect({ prNum: PR, isPublic: false }).toExistAsABuild();
});
it('should reject if verification fails', async () => {
const BUILD = BuildNums.TRUST_CHECK_ERROR;
const PR = PrNums.TRUST_CHECK_ERROR;
await circleBuild(payload(BUILD)).then(h.verifyResponse(500));
expect({ prNum: PR }).toExistAsAnArtifact();
expect({ prNum: PR }).not.toExistAsABuild();
expect({ prNum: PR, isPublic: false }).not.toExistAsABuild();
});
it('should be able to notify that a PR has been updated (and do nothing)', async () => {
await prUpdated(PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER).then(h.verifyResponse(200));
// The PR should still not exist.
expect({ prNum: PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER, isPublic: false }).not.toExistAsABuild();
expect({ prNum: PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER, isPublic: true }).not.toExistAsABuild();
});
});
describe('for an existing PR', () => {
it('should be able to create and serve a public preview', async () => {
const BUILD = BuildNums.TRUST_CHECK_ACTIVE_TRUSTED_USER;
const PR = PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER;
const regexPrefix1 = `^PR: ${PR} \\| SHA: ${ALT_SHA} \\| File:`;
const idxContentRegex1 = new RegExp(`${regexPrefix1} \\/index\\.html$`);
const barContentRegex1 = new RegExp(`${regexPrefix1} \\/foo\\/bar\\.js$`);
const regexPrefix2 = `^BUILD: ${BUILD} \\| PR: ${PR} \\| SHA: ${SHA} \\| File:`;
const idxContentRegex2 = new RegExp(`${regexPrefix2} \\/index\\.html$`);
const barContentRegex2 = new RegExp(`${regexPrefix2} \\/foo\\/bar\\.js$`);
h.createDummyBuild(PR, ALT_SHA);
await circleBuild(payload(BUILD)).then(h.verifyResponse(201));
await Promise.all([
getFile(PR, ALT_SHA, 'index.html').then(h.verifyResponse(200, idxContentRegex1)),
getFile(PR, ALT_SHA, 'foo/bar.js').then(h.verifyResponse(200, barContentRegex1)),
getFile(PR, SHA, 'index.html').then(h.verifyResponse(200, idxContentRegex2)),
getFile(PR, SHA, 'foo/bar.js').then(h.verifyResponse(200, barContentRegex2)),
]);
expect({ prNum: PR, sha: SHA }).toExistAsABuild();
expect({ prNum: PR, sha: ALT_SHA }).toExistAsABuild();
});
it('should be able to create but not serve a hidden preview', async () => {
const BUILD = BuildNums.TRUST_CHECK_UNTRUSTED;
const PR = PrNums.TRUST_CHECK_UNTRUSTED;
h.createDummyBuild(PR, ALT_SHA, false);
await circleBuild(payload(BUILD)).then(h.verifyResponse(202));
await Promise.all([
getFile(PR, ALT_SHA, 'index.html').then(h.verifyResponse(404)),
getFile(PR, ALT_SHA, 'foo/bar.js').then(h.verifyResponse(404)),
getFile(PR, SHA, 'index.html').then(h.verifyResponse(404)),
getFile(PR, SHA, 'foo/bar.js').then(h.verifyResponse(404)),
]);
expect({ prNum: PR, sha: SHA }).not.toExistAsABuild();
expect({ prNum: PR, sha: SHA, isPublic: false }).toExistAsABuild();
expect({ prNum: PR, sha: ALT_SHA }).not.toExistAsABuild();
expect({ prNum: PR, sha: ALT_SHA, isPublic: false }).toExistAsABuild();
});
it('should reject if verification fails', async () => {
const BUILD = BuildNums.TRUST_CHECK_ERROR;
const PR = PrNums.TRUST_CHECK_ERROR;
h.createDummyBuild(PR, ALT_SHA, false);
await circleBuild(payload(BUILD)).then(h.verifyResponse(500));
expect({ prNum: PR }).toExistAsAnArtifact();
expect({ prNum: PR }).not.toExistAsABuild();
expect({ prNum: PR, isPublic: false }).not.toExistAsABuild();
expect({ prNum: PR, sha: ALT_SHA, isPublic: false }).toExistAsABuild();
});
it('should not be able to overwrite an existing public preview', async () => {
const BUILD = BuildNums.TRUST_CHECK_ACTIVE_TRUSTED_USER;
const PR = PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER;
const regexPrefix = `^PR: ${PR} \\| SHA: ${SHA} \\| File:`;
const idxContentRegex = new RegExp(`${regexPrefix} \\/index\\.html$`);
const barContentRegex = new RegExp(`${regexPrefix} \\/foo\\/bar\\.js$`);
h.createDummyBuild(PR, SHA);
await circleBuild(payload(BUILD)).then(h.verifyResponse(409));
await Promise.all([
getFile(PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER, SHA, 'index.html').then(h.verifyResponse(200, idxContentRegex)),
getFile(PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER, SHA, 'foo/bar.js').then(h.verifyResponse(200, barContentRegex)),
]);
expect({ prNum: PR }).toExistAsAnArtifact();
expect({ prNum: PR }).toExistAsABuild();
});
it('should not be able to overwrite an existing hidden preview', async () => {
const BUILD = BuildNums.TRUST_CHECK_UNTRUSTED;
const PR = PrNums.TRUST_CHECK_UNTRUSTED;
h.createDummyBuild(PR, SHA, false);
await circleBuild(payload(BUILD)).then(h.verifyResponse(409));
expect({ prNum: PR }).toExistAsAnArtifact();
expect({ prNum: PR, isPublic: false }).toExistAsABuild();
});
it('should be able to request re-checking visibility (if outdated)', async () => {
const publicPr = PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER;
const hiddenPr = PrNums.TRUST_CHECK_UNTRUSTED;
h.createDummyBuild(publicPr, SHA, false);
h.createDummyBuild(hiddenPr, SHA, true);
// PR visibilities are outdated (i.e. the opposte of what the should).
expect({ prNum: publicPr, sha: SHA, isPublic: false }).toExistAsABuild(false);
expect({ prNum: publicPr, sha: SHA, isPublic: true }).not.toExistAsABuild(false);
expect({ prNum: hiddenPr, sha: SHA, isPublic: false }).not.toExistAsABuild(false);
expect({ prNum: hiddenPr, sha: SHA, isPublic: true }).toExistAsABuild(false);
await Promise.all([
prUpdated(publicPr).then(h.verifyResponse(200)),
prUpdated(hiddenPr).then(h.verifyResponse(200)),
]);
// PR visibilities should have been updated.
expect({ prNum: publicPr, isPublic: false }).not.toExistAsABuild();
expect({ prNum: publicPr, isPublic: true }).toExistAsABuild();
expect({ prNum: hiddenPr, isPublic: false }).toExistAsABuild();
expect({ prNum: hiddenPr, isPublic: true }).not.toExistAsABuild();
});
it('should be able to request re-checking visibility (if up-to-date)', async () => {
const publicPr = PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER;
const hiddenPr = PrNums.TRUST_CHECK_UNTRUSTED;
h.createDummyBuild(publicPr, SHA, true);
h.createDummyBuild(hiddenPr, SHA, false);
// PR visibilities are already up-to-date.
expect({ prNum: publicPr, sha: SHA, isPublic: false }).not.toExistAsABuild(false);
expect({ prNum: publicPr, sha: SHA, isPublic: true }).toExistAsABuild(false);
expect({ prNum: hiddenPr, sha: SHA, isPublic: false }).toExistAsABuild(false);
expect({ prNum: hiddenPr, sha: SHA, isPublic: true }).not.toExistAsABuild(false);
await Promise.all([
prUpdated(publicPr).then(h.verifyResponse(200)),
prUpdated(hiddenPr).then(h.verifyResponse(200)),
]);
// PR visibilities are still up-to-date.
expect({ prNum: publicPr, isPublic: true }).toExistAsABuild();
expect({ prNum: publicPr, isPublic: false }).not.toExistAsABuild();
expect({ prNum: hiddenPr, isPublic: true }).not.toExistAsABuild();
expect({ prNum: hiddenPr, isPublic: false }).toExistAsABuild();
});
it('should reject a request if re-checking visibility fails', async () => {
const errorPr = PrNums.TRUST_CHECK_ERROR;
h.createDummyBuild(errorPr, SHA, true);
expect({ prNum: errorPr, isPublic: false }).not.toExistAsABuild(false);
expect({ prNum: errorPr, isPublic: true }).toExistAsABuild(false);
await prUpdated(errorPr).then(h.verifyResponse(500, /TRUST_CHECK_ERROR/));
// PR visibility should not have been updated.
expect({ prNum: errorPr, isPublic: false }).not.toExistAsABuild();
expect({ prNum: errorPr, isPublic: true }).toExistAsABuild();
});
it('should reject a request if updating visibility fails', async () => {
// One way to cause an error is to have both a public and a hidden directory for the same PR.
h.createDummyBuild(PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER, SHA, false);
h.createDummyBuild(PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER, SHA, true);
const hiddenPrDir = h.getPrDir(PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER, false);
const publicPrDir = h.getPrDir(PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER, true);
const bodyRegex = new RegExp(`Request to move '${hiddenPrDir}' to existing directory '${publicPrDir}'`);
expect({ prNum: PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER, isPublic: false }).toExistAsABuild(false);
expect({ prNum: PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER, isPublic: true }).toExistAsABuild(false);
await prUpdated(PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER).then(h.verifyResponse(409, bodyRegex));
// PR visibility should not have been updated.
expect({ prNum: PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER, isPublic: false }).toExistAsABuild();
expect({ prNum: PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER, isPublic: true }).toExistAsABuild();
});
});
}));
@@ -0,0 +1,2 @@
import '../preview-server';
import './mock-external-apis';
@@ -0,0 +1,30 @@
declare module 'tar-stream' {
import {Readable, Writable} from 'stream';
export interface Pack extends Readable {
entry(header: Header, callback?: (err?: any) => {}): Writable;
entry(header: Header, contents: string, callback?: (err?: any) => {}): Writable;
entry(header: Header, buffer: Buffer, callback?: (err?: any) => {}): Writable;
entry(header: Header, buffer: string|Buffer, callback?: (err?: any) => {}): Writable;
finalize();
destroy(err: any);
}
export interface Header {
name: string;
mode?: number;
uid?: number;
gid?: number;
size?: number;
mtime?: Date;
type?: type;
linkname?: string;
uname?: string;
gname?: string;
devmajor?: number;
devminor?: number;
}
export function pack(): Pack;
}
@@ -0,0 +1,54 @@
{
"name": "aio-scripts-js",
"version": "1.0.0",
"description": "Performing various tasks on PR build artifacts for angular.io.",
"repository": "https://github.com/angular/angular.git",
"author": "Angular",
"license": "MIT",
"scripts": {
"prebuild": "yarn clean-dist",
"build": "yarn ~~build",
"prebuild-watch": "yarn prebuild",
"build-watch": "yarn ~~build-watch",
"clean-dist": "node --eval \"require('shelljs').rm('-rf', 'dist')\"",
"predev": "yarn build || true",
"dev": "run-p ~~build-watch ~~test-watch",
"lint": "tslint --project tsconfig.json",
"pretest": "run-s build lint",
"test": "yarn ~~test-only",
"pretest-watch": "yarn pretest",
"test-watch": "yarn ~~test-watch",
"~~build": "tsc",
"~~build-watch": "yarn ~~build --watch",
"~~test-only": "node dist/test",
"~~test-watch": "nodemon --delay 1 --exec \"yarn ~~test-only\" --watch dist"
},
"dependencies": {
"body-parser": "^1.19.0",
"delete-empty": "^3.0.0",
"express": "^4.17.1",
"jasmine": "^4.0.0",
"nock": "^13.0.4",
"node-fetch": "^2.6.1",
"shelljs": "^0.8.5",
"source-map-support": "^0.5.19",
"tar-stream": "^2.1.3",
"tslib": "^2.3.0"
},
"devDependencies": {
"@types/body-parser": "^1.19.0",
"@types/express": "^4.17.8",
"@types/jasmine": "^4.0.0",
"@types/nock": "^11.1.0",
"@types/node": "^14.6.4",
"@types/node-fetch": "^2.5.7",
"@types/shelljs": "^0.8.8",
"@types/supertest": "^2.0.10",
"nodemon": "^2.0.4",
"npm-run-all": "^4.1.5",
"supertest": "^6.0.0",
"tslint": "^6.1.3",
"tslint-jasmine-noSkipOrFocus": "^1.0.9",
"typescript": "~4.7.2"
}
}

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