Compare commits

...

336 Commits

Author SHA1 Message Date
Andrew Kushnir 5d5b51599d release: cut the v15.0.3 release 2022-12-07 13:54:02 -08:00
Konstantin Kharitonov bf44dc234a fix(common): Update Location to support base href containing origin (#48327)
In case `APP_BASE_HREF` is set including `origin` the further usage of it might cause failure

e.g.
If an app is placed on `https://example.com` and bundles are on `https://cdn-example.com` you have to set `APP_BASE_HREF` up as `https://example.com/` and build the app with `--base-href` as `https://cdn-example.com/` but it does not work because of the bug

Fixes #48175

PR Close #48327
2022-12-07 09:18:01 -08:00
Angular Robot 7df1989940 build: update cross-repo angular dependencies (#47882) (#48384)
See associated pull request for more information.

PR Close #47882

(cherry picked from commit 7c4e9ce5c0)

PR Close #48384
2022-12-07 09:09:30 -08:00
Angular Robot 6b6bf6332b build: update all non-major dependencies (#48372) (#48384)
See associated pull request for more information.

PR Close #48372

(cherry picked from commit 126573d76b)

PR Close #48384
2022-12-07 09:09:30 -08:00
Alan Agius cb7365a7bd refactor(compiler): replace flatten and map with flatMap. (#48378)
Replace custom `flatten` and `map` with native `flatMap` usage.

Benchmark:
| Test case name 	| Result                                                 	|
|----------------	|--------------------------------------------------------	|
| flatten & map  	| flatten & map x 1,182 ops/sec ±2.18% (63 runs sampled) 	|
| flatMap        	| flatMap x 6,011 ops/sec ±0.91% (35 runs sampled)       	|

The fact that `flatMap` is faster is also highlighted in https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/flatMap

PR Close #48378
2022-12-07 09:08:26 -08:00
Angular Robot a51e625239 build: update github/codeql-action action to v2.1.35 (#48371)
See associated pull request for more information.

PR Close #48371
2022-12-07 09:07:34 -08:00
Kristiyan Kostadinov e4dcaa513e fix(core): unable to inject ChangeDetectorRef inside host directives (#48355)
When injecting the `ChangeDetectorRef` into a node that matches a component, we create a new ref using the component's LView. This breaks down for host directives, because they run before the component's LView has been created.

These changes resolve the issue by creating the LView before creating the node injector for the directives.

Fixes #48249.

PR Close #48355
2022-12-07 09:06:46 -08:00
Alan Agius c0d04175ff refactor(core): simplify array flatten logic (#48358)
We can now use modern Javascript to get the same result.

PR Close #48358
2022-12-06 12:48:42 -08:00
Angular Robot 5e2cd07a0b build: update dependency jasmine-spec-reporter to v7 (#48367)
See associated pull request for more information.

PR Close #48367
2022-12-06 11:43:05 -08:00
Angular Robot 4c76f94ce0 build: update dependency @types/jasmine to v4 (#48365)
See associated pull request for more information.

PR Close #48365
2022-12-06 10:57:04 -08:00
Angular Robot 0e539eb56d build: update eslint dependencies to v5.45.1 (#48223)
See associated pull request for more information.

PR Close #48223
2022-12-06 10:52:16 -08:00
Angular Robot 39b6cbcf4b build: update dependency karma-jasmine-html-reporter to v2 (#48369)
See associated pull request for more information.

PR Close #48369
2022-12-06 10:09:14 -08:00
dario-piotrowicz 9a5d84249a fix(compiler): make sure selectors inside container queries are correctly scoped (#48353)
improve the emulated shadowDom implementation so that it can correctly
scope selectors present inside the @container at-rule (recently added
to the css specs)

resolves #48264

PR Close #48353
2022-12-06 09:59:00 -08:00
Alan Agius 1e58a8c05d docs: wrap json property in double quotes (#48375)
Previously, the `budgets` was not wrapped in quotes which caused the JSON to be invalid.

PR Close #48375
2022-12-06 09:58:22 -08:00
Alan Agius 40d6a06615 refactor(core): remove old AJD workaround for G3 (#48363)
This workaround does not seem to be needed any longer.

PR Close #48363
2022-12-06 09:14:24 -08:00
Angular Robot fc872b12aa build: update dependency ts-node to v10 (#48370)
See associated pull request for more information.

PR Close #48370
2022-12-06 09:12:27 -08:00
Angular Robot fdb2a38a1b build: update dependency karma-jasmine to v5 (#48368)
See associated pull request for more information.

PR Close #48368
2022-12-06 09:11:48 -08:00
Angular Robot ac51e5c107 build: update dependency jasmine-core to v4 (#48366)
See associated pull request for more information.

PR Close #48366
2022-12-05 15:56:41 -08:00
Paul Gschwendtner 45cc5f49e4 build: switch new AIO preview action to use aio: preview label (#48345)
The new AIO preview deployment should take over the existing `aio:
preview` label- now after it being tested- and working as expected.

PR Close #48345
2022-12-05 14:52:38 -08:00
Paul Gschwendtner 3f9cfae8a4 build: remove docker-based AIO preview deploy setup (#48345)
The docker-based AIO preview deploy setup is replaced with
significantly simpler setup using GitHub actions provided by
dev-infra (also something dev-infra can maintain better then).

The actions under the hood leverage Firebase preview channels.

PR Close #48345
2022-12-05 14:52:38 -08:00
Bob Watson dcab0f48d6 docs: add example app for new landing page (#48360)
PR Close #48360
2022-12-05 14:37:47 -08:00
Alan Agius 0b04da1fd7 refactor(core): remove several private utils and APIs (#48357)
These APIs are no longer used.

PR Close #48357
2022-12-05 14:35:10 -08:00
Alan Agius a4c82ba25b refactor(compiler): remove unused projection const (#48357)
`projection` const is unused in `serializePlaceholderValue`.

PR Close #48357
2022-12-05 14:35:09 -08:00
Alan Agius 28b0b89cb2 refactor(common): remove unused setDOM (#48357)
Remove unused `setDOM` method is unused.

PR Close #48357
2022-12-05 14:35:09 -08:00
Alex Castle 50b1c2bf52 fix(common): Don't generate srcsets with very large sources (#47997)
Fix an issue where users could inadvertently generate very large source images in ngOptimizedImage

PR Close #47997
2022-12-05 12:12:14 -08:00
Joey Perrott 9214e58d44 build: enable the ng-dev auth service (#48361)
Enable the auth service for merging into the repository

PR Close #48361
2022-12-05 12:09:08 -08:00
Paul Gschwendtner 0b388a00da build: add more debug information for occasional windows flakiness (#48351)
Ocassionally the NodeJS version is not correct after NVM on Windows.
This adds some debug information that should help improve stability
here. A recent attempt to update the `$PATH` did not seem to help.

PR Close #48351
2022-12-05 12:04:05 -08:00
piyush132000 dc5f436769 docs: removed empty methods (#48343)
Removed empty methods and import from files

PR Close #48343
2022-12-05 12:03:33 -08:00
Derek Cormier 7f31e55961 build(bazel): mark an AIO sjs e2e test as flakey (#48331)
Like a couple of the the other systemjs e2e tests, this one occasionally fails CI.

PR Close #48331
2022-12-05 12:02:29 -08:00
Andrew Scott 06c34cd99a refactor(router): Remove assignExtraOptionsToRouter helper function (#48313)
This commit removes the unnecessary `assignExtraOptionsToRouter` helper
function. This helper is not necessary because the options can be
retrieved from DI and assigned during the construction on the Router.

PR Close #48313
2022-12-05 12:01:57 -08:00
Andrew Scott 58e55495f2 refactor(router): Eliminate constructor parameters in Router class (#48313)
The Router constructor and `setupRouter` factory mainly exist as a
legacy configuration. Since the Router's creation, the style in Angular
has evolved quite a bit. This commit eliminates and cleans up some
unnecessary comlicated code paths related to the router
constructor/factory.

Note that there are edits to the `setupTestingRouter` that could be seen
as breaking. However, it is not for several reasons:

1. The function is documented as a factory function. If used as
   documented, the parameters should match what's available in DI
2. The function is totally unused by the Router itself and is not used
   in g3 either. I believe it was made publicApi by an error when
   updating documentation annotations long ago.

PR Close #48313
2022-12-05 12:01:57 -08:00
Derek Cormier 1b373067e1 build(bazel): set ci steps that build aio to 2xlarge+ (#48342)
These steps builds AIO which is memory intensive. Running out of memory
may be the cause of some ci flakiness. Change the executor size to test
this theory.

For context: The CLI process is randomly getting `Killed`, likely by the
linux OOM killer. The Angular CLI builds happen on the host machine- no RBE.

Without the Bazel migration they were previously already at ~63% peak RAM.
With Bazel's overhead and e.g. remote caching, it seems to hit 100%
(can be seen in CircleCI machine insights). OOM killer may then choose the
highest consuming task. i.e. the Angular CLI and kill it.

Angular CLI build ram consumption is proportional to the size of the app. AIO is not small

https://app.circleci.com/pipelines/github/angular/angular/53992/workflows/ec47e3d8-2e7d-45b8-a802-7ec8ec4026c4/jobs/1265666/parallel-runs/0/steps/0-106

https://angular-team.slack.com/archives/C02PARQNMC1/p1670067910332809i

PR Close #48342
2022-12-05 11:40:56 +00:00
Paul Gschwendtner 226441fc3a build: set target firebase project when setting deploy target (#48352)
We now set the Firebase deploy target for AIO preview deployment
via GitHub actions. It looks like configuring the deploy target
also requires authentication / or a project.

PR Close #48352
2022-12-04 20:26:10 +00:00
Paul Gschwendtner 38701ce07b build: check out project before setting AIO preview targets (#48347)
Checks out the Firebase project before setting the AIO preview targets.
Surprisingly Firebase requires a project when setting resource
identifiers for the targets. This is already done by the Firebase
deploy action from @FirebaseExtended, but we need to do it earlier.

PR Close #48347
2022-12-04 16:14:11 +00:00
Paul Gschwendtner 09388eb9ab build: use npx for setting firebase deploy targets in AIO preview (#48346)
For the deploy job we still need to use the Firebase CLI to configure
the proper hosting deploy targets. See:
011ef66a04.

We don't have node modules installed in this job, and it would
also be overkill to install them for just using firebase here.

Since the action uses `npx firebase-tools` down the line anyway,
we can use it without any speed downsides.

PR Close #48346
2022-12-04 13:18:40 +00:00
Paul Gschwendtner 95886d8aa9 build: set firebase deploy target when deploying AIO preview (#48338)
The AIO firebase project specifies `aio` as deploy target. This means
that we need to configure this target before being able to deploy AIO
previews.

We use the default project target as target since the preview Firebase
project does not use multi site targets. See:
https://firebase.google.com/docs/hosting/multisites

PR Close #48338
2022-12-04 11:11:40 +00:00
Angular Robot 6f79c2b7fd build: update actions/checkout action to v3 (#48337)
See associated pull request for more information.

PR Close #48337
2022-12-03 16:09:10 +00:00
Angular Robot 59f2d46a16 build: update actions/checkout digest (#48336)
See associated pull request for more information.

PR Close #48336
2022-12-03 10:45:39 +00:00
Paul Gschwendtner 30226f08f3 build: setup github actions for preview deployment of AIO (#48302)
Sets up two new actions for supporting preview deployments
of AIO by applying a label. i.e. `AIO preview`.

PR Close #48302
2022-12-03 10:13:26 +00:00
Paul Gschwendtner 7bc6c2d314 build: introduce common action for installing dependencies w/ cache in actions (#48302)
Instead of repeating the node module installation step in future GitHub
actions, a single place should contain the cache key etc.

PR Close #48302
2022-12-03 10:13:26 +00:00
Paul Gschwendtner dd4fd1f4f4 build: attempt to avoid nodejs version flakiness in test_win job (#48326)
The `test_win` job is occasionally failing where a super old NodeJS
version is used even though we installed a proper one with `nvm`.

It's unclear how the CircleCI windows VMs differ here, it's coming
up rarely and is hard to reproduce when SSH-ing into VMs. Our best
guess is that the existing NodeJS version from the container/image is
super old and somehow takes precedence over the NVM-managed NodeJS
version. This could happen due to bad ordering in the `$PATH`
environment variable, or NVM not properly being wired up there.

We attempt to fix this by always setting `$PATH` to the NVM symlink
directory manually- with highest priority.

PR Close #48326
2022-12-02 14:55:55 -08:00
Andrew Kushnir 49af83e5e3 docs: added missingNgForOfLet extended diagnostics details page (#48320)
Closes #48288.

PR Close #48320
2022-12-02 14:54:10 -08:00
Andrew Kushnir 2131c551cd docs: added missingControlFlowDirective extended diagnostics details page (#48319)
Closes #48287.

PR Close #48319
2022-12-02 11:46:54 -08:00
Alan Agius 2af638818a refactor(platform-browser): simplify flattenStyles method (#48293)
With this change we simplify the `flattenStyles` logic using modern JavaScript.

PR Close #48293
2022-12-02 11:36:13 -08:00
Paul Gschwendtner 2eccfd412e build: fix unnecessary large runfiles in AIO when composing boilerplate (#48329)
The `example-boilerplate` NodeJS binary always depends on all files from
the individual examples. This is not needed as for composition w/ the
boilerplate, only the target example content + boilerplate is needed
in the runfiles. This reduces the runfiles and therefore speeds up
boilerplate creation (which can happen for all examples concurrently).

PR Close #48329
2022-12-02 09:47:44 -08:00
Paul Gschwendtner f1cb455c1e build: always ensure node_modules folders are not picked up in AIO (#48329)
AIO has two risk places where `node_modules` folders may exist and may
be accidentally picked up by Bazel- even though they are already added
as part of `.bazelignore` and made visible through `.gitignore`. It
doesn't hurt instructing Bazel to always ignore `node_modules` as part
of the `glob` instruction.

Co-authored-by: Joey Perrott <josephperrott@gmail.com>

PR Close #48329
2022-12-02 09:47:44 -08:00
Paul Gschwendtner 520946d2e7 build: do not use --config=release when building AIO for development (#48329)
When building AIO using `yarn build` the `--config=release` is added.
This was done as part of the Bazel migration to make sure the footer
shows a Git revision via Bazel stamping.

This does not provide enough benefits, compared to the downside of
reduced caching. Bazel will discard the analysis cache when the stamp
configuration changes.

This may happen easily when you work in framework where
`--config=release` is not used. Then when starting work in AIO: the
config changes and the cache is discarded. This may even mean that
existing framework package build artifacts may be discarded when an
AIO local build is started.

Co-authored-by: Joey Perrott <josephperrott@gmail.com>

PR Close #48329
2022-12-02 09:47:44 -08:00
Paul Gschwendtner cd526c1db6 build: cleanup bazelignore file (#48329)
These folders are no longer used in the source tree, and only exist
in the Bazel sandbox. We can rmeove them. Node modules are kept because
people may create them when editing the integration `package.json`
files.

PR Close #48329
2022-12-02 09:47:43 -08:00
Paul Gschwendtner ff07194e92 build: avoid common node_modules/ bazel slowness pitfall (#48329)
It's very easy to have some random `node_modules/` folder floating
around in the repository. Especially coming from the old AIO
example boilerplate setup (which heavily relied on nested node modules).

These `node_modules/` folders can contain millions of files and might
be accidentally picked up by Bazel `glob`'s (if they are authored
in way that selects all nested files).

This could surface in `aio/tools/examples/shared` or individual content
examples, causing the Bazel analysis phase or runfile forest creation
to take forever given more than 1.5 files being dealt with.

Note that we keep the `examples/shared` node modules folder gitignored
as otherwise Renovate may have problems when updating the folder.

PR Close #48329
2022-12-02 09:47:43 -08:00
Andrew Scott c0fe82049f docs: Add page for 'suffixNotSupported' diagnostic (#48315)
Adds documentation page for `suffixNotSupported` extended diagnostic.

fixes #48290

PR Close #48315
2022-12-02 08:59:01 -08:00
Pawel Kozlowski fab1af764d perf(docs-infra): remove usage of the NgClass directive (#48312)
NgClass has non-negligable performance cost, especially if a
different object is provided as NgClass input (which was the case
for the NavItemComponent). The perfornace problem was noticed
in the scope of the INP score investigations while profiling the
https://angular.io/resources page. On this particular page replacing
NgClass usage with alternatives results in 20x (!) runtime perf
improvement. Such big improvement is possible since we avoid unneeded
CSS classes removal / adding in the DOM.

Part of #25518

PR Close #48312
2022-12-02 08:58:20 -08:00
Paul Gschwendtner 5a85e440a8 build: enable progress messages for Bazel in CircleCI (#48316)
Bazel otherwise may almost look as being stuck when it performs
many slow e2e tests. We should show what is running, even it results
in a little more output in the logs.

PR Close #48316
2022-12-02 08:57:45 -08:00
Paul Gschwendtner 6921bf210a build: patch rules_nodejs to ensure nodejs_binary targets can be used as tools in RBE (#48316)
See full explanation in: https://github.com/bazelbuild/rules_nodejs/pull/3605

PR Close #48316
2022-12-02 08:57:44 -08:00
Paul Gschwendtner b9ef61906e build: fix AIO jobs not running with RBE and CircleCI Bazel settings (#48316)
The `.bazelrc` workspace file attempted to imported the `.bazelrc.user`
file. This user file is set on CI to include the CI specific Bazel
settings.

Apparently this user rc import does not work, and fails silently given
the `try-import`. It fails because for AIO Bazel commands we change
the working directory using `yarn --cwd aio` and the `try-import`
is relative to the working directory..

PR Close #48316
2022-12-02 08:57:44 -08:00
Andrew Scott 167bc0d163 fix(compiler-cli): Produce diagnostic rather than crash when using invalid hostDirective (#48314)
Because the language service uses the compiler, we try to produce as
much useful information as possible rather than throwing hard errors.
Hard errors cause the compiler to crash. While this can be acceptable
when compiling a program as part of a regular build, this is undesirable
for the language service.

PR Close #48314
2022-12-01 13:43:31 -08:00
piyush132000 b620d9b316 docs: removed empty method (#48236)
I removed empty methods from a file and unnecessary import and added export keyword for exportng module

PR Close #48236
2022-12-01 09:39:09 -08:00
Andrew Scott 73ec0c61fa release: cut the v15.0.2 release 2022-11-30 12:20:17 -08:00
Pawel Kozlowski d933b17170 refactor(core): remove debug data structures (#48281)
This commit removes several debug data structures from
the framework runtime. The data structures in question
were introduced in the framework in the past with the
idea of having debugging aid in the form of the human-redable
data structures. It turned out that in practice those
data structures were not used (most of the fwk developers
didn't even know about their existence!), yet we kept
paying the price of maintaining those duplicated (prod
and debug) version of the data structures.

PR Close #48281
2022-11-30 09:27:59 -08:00
Angular Robot db53ce40c5 build: update dependency @types/uuid to v9 (#48285)
See associated pull request for more information.

PR Close #48285
2022-11-30 09:27:04 -08:00
Andrew Scott 2a445923ee refactor(router): Remove DeprecatedLoadChildren (#48292)
This option has already been removed from availability externally. Internally, the last use has been removed
for the type on Route.loadChildren. More testing will be necessary to verify nothing relies on the other
code paths.

PR Close #48292
2022-11-30 08:29:11 -08:00
Andrew Scott 82756e8a5f refactor(router): Move navigationId handling to the transition manager (#48257)
The navigationId is really just a count of how many navigations have been
processed through the navigation pipeline. This tracking should be
done as part of the navigation transition handler

PR Close #48257
2022-11-29 14:55:53 -08:00
Andrew Scott 60af2d091c refactor(router): Move transition subject into transition handler (#48257)
The navigation transitions subject belongs in the navigation transitions handler
rather than the Router.

PR Close #48257
2022-11-29 14:55:53 -08:00
Andrew Scott 92066f162d refactor(router): directly inject internal properties from DI (#48257)
Rather than accessing router internal properties that are available in DI,
this commit directly injects those dependencies in the navigation transition
handler. Note that there are several other items that _should_ be injectable
in DI (like the TitleStrategy) but because those are part of the Router public
interface and writeable(!), they might be changed during runtime. We
have to get these other items from the Router for now...

PR Close #48257
2022-11-29 14:55:53 -08:00
Andrew Scott 0a36f4fbc8 refactor(router): Get EnvironmentInjector directly in transition handler (#48257)
Remove the NgModuleRef from the internal interface of the Router. This
was only used to get the injector (equivalent to EnvironmentInjector).
Instead, the navigation transition handler can directly inject the
EnvironmentInjector

PR Close #48257
2022-11-29 14:55:53 -08:00
Andrew Scott 8e46fb5676 refactor(router): Move configLoader to transition handler (#48257)
The ConfigLoader is only used as part of the navigation transition.
Moving the loader there completes the removal of event triggering in
the router code. This all now lives in the transition handler (plus
the router scroller, but let's ignore that...).

PR Close #48257
2022-11-29 14:55:53 -08:00
Andrew Scott f36cf49330 refactor(router): Move events subject to transition class (#48257)
The events observable is part of the lifecycle of a navigation. This should
be contained in the NavigationTransitions class rather than the Router.
This Subject is re-exported by the router as an observable to maintain
the public API

PR Close #48257
2022-11-29 14:55:52 -08:00
Andrew Scott 2409dd6530 refactor(router): remove malformedUriHandler from interface passed to transitions (#48257)
The malformedUriHandler is not used during the navigation transition and can be removed form the interface

PR Close #48257
2022-11-29 14:55:52 -08:00
Angular Robot 2067643896 build: update eslint dependencies (#48216)
See associated pull request for more information.

PR Close #48216
2022-11-29 12:29:47 -08:00
Alan Agius a3bbc6dcd9 build: remove codelyzer from aio dependencies (#48216)
This package is no longer used.

PR Close #48216
2022-11-29 12:29:47 -08:00
Alan Agius 3c62c70caf build: update cross-repo dependencies to latest stable version (#48216)
With this change we update the cross-repo dependencies to the latest stable version

PR Close #48216
2022-11-29 12:29:46 -08:00
Angular Robot fb4f228d3c build: update github/codeql-action action to v2.1.34 (#48272)
See associated pull request for more information.

PR Close #48272
2022-11-29 12:29:20 -08:00
Rokas Brazdžionis bec0007c82 docs: update date pipe default timezone injection token reference (#48274)
replace deprecated `DATE_PIPE_DEFAULT_TIMEZONE` with `DATE_PIPE_DEFAULT_OPTIONS`
PR Close #48274
2022-11-29 09:40:28 -08:00
Paul Gschwendtner 4df7301093 build: hide --loader warning when ESM import patch is installed (#48282)
Similar to the Rules NodeJS require patch, we have an ESM import patch
as of the AIO Bazel migration (to support ESM scripts better).

This script uses `--loader`, an experimental NodeJS flag. This is
similar to how `ts-node` uses it. We should disable the warnings
as it results in a lot of unreadable Bazel output and the warnings
are okay to be ignored. Note that we cannot fine-grain disable
the specific warning so all others would be disabled too.

Realistically we haven't seen any in the past and long-term we will
be not relying on patched resolution anyway (looking at `rules_js`).

PR Close #48282
2022-11-29 09:39:26 -08:00
Andrew Scott a192dea677 Revert "refactor(router): remove malformedUriHandler from interface passed to transitions (#48257)" (#48270)
This reverts commit c20a7eeba6.

PR Close #48270
2022-11-28 16:58:53 -08:00
Andrew Scott d29cbe885c Revert "refactor(router): Move events subject to transition class (#48257)" (#48270)
This reverts commit f88559eb48.

PR Close #48270
2022-11-28 16:58:53 -08:00
Andrew Scott 1eef647236 Revert "refactor(router): Move configLoader to transition handler (#48257)" (#48270)
This reverts commit 8c8ded8d31.

PR Close #48270
2022-11-28 16:58:53 -08:00
Andrew Scott 9745fac73f Revert "refactor(router): Get EnvironmentInjector directly in transition handler (#48257)" (#48270)
This reverts commit 03519968c0.

PR Close #48270
2022-11-28 16:58:52 -08:00
Andrew Scott 7032bf0780 Revert "refactor(router): directly inject internal properties from DI (#48257)" (#48270)
This reverts commit ce38759f54.

PR Close #48270
2022-11-28 16:58:52 -08:00
Andrew Scott 88aec875c0 Revert "refactor(router): Move transition subject into transition handler (#48257)" (#48270)
This reverts commit 0d88348430.

PR Close #48270
2022-11-28 16:58:52 -08:00
Andrew Scott f2d20d5c50 Revert "refactor(router): Move navigationId handling to the transition manager (#48257)" (#48270)
This reverts commit ea114a0034.

PR Close #48270
2022-11-28 16:58:52 -08:00
Bob Watson bd59a1a395 docs: update docs to use new build and test commands (#48266)
PR Close #48266
2022-11-28 15:33:53 -08:00
Matthieu Riegler c0601db72c test(core): remove some ViewEngine related test code (#48238)
Some tests had comments relating to ViewEngine that were not needed anymore since VE was dropped.

PR Close #48238
2022-11-28 13:08:23 -08:00
Matthieu Riegler aa920aa89a refactor(router): remove non-null assertions (#48244)
As part of #24571, removing non-null assertions (where appropriate)

PR Close #48244
2022-11-28 13:07:59 -08:00
Derek Cormier 6eb2f76fee build(bazel): fix aio deploy script
Move the copy of the bazel dist folder to the firebase dist folder
earlier on in the process so that any checks operate out of a single
folder.
2022-11-28 13:29:35 -07:00
Andrew Scott ea114a0034 refactor(router): Move navigationId handling to the transition manager (#48257)
The navigationId is really just a count of how many navigations have been
processed through the navigation pipeline. This tracking should be
done as part of the navigation transition handler

PR Close #48257
2022-11-28 11:54:43 -08:00
Andrew Scott 0d88348430 refactor(router): Move transition subject into transition handler (#48257)
The navigation transitions subject belongs in the navigation transitions handler
rather than the Router.

PR Close #48257
2022-11-28 11:54:43 -08:00
Andrew Scott ce38759f54 refactor(router): directly inject internal properties from DI (#48257)
Rather than accessing router internal properties that are available in DI,
this commit directly injects those dependencies in the navigation transition
handler. Note that there are several other items that _should_ be injectable
in DI (like the TitleStrategy) but because those are part of the Router public
interface and writeable(!), they might be changed during runtime. We
have to get these other items from the Router for now...

PR Close #48257
2022-11-28 11:54:43 -08:00
Andrew Scott 03519968c0 refactor(router): Get EnvironmentInjector directly in transition handler (#48257)
Remove the NgModuleRef from the internal interface of the Router. This
was only used to get the injector (equivalent to EnvironmentInjector).
Instead, the navigation transition handler can directly inject the
EnvironmentInjector

PR Close #48257
2022-11-28 11:54:43 -08:00
Andrew Scott 8c8ded8d31 refactor(router): Move configLoader to transition handler (#48257)
The ConfigLoader is only used as part of the navigation transition.
Moving the loader there completes the removal of event triggering in
the router code. This all now lives in the transition handler (plus
the router scroller, but let's ignore that...).

PR Close #48257
2022-11-28 11:54:43 -08:00
Andrew Scott f88559eb48 refactor(router): Move events subject to transition class (#48257)
The events observable is part of the lifecycle of a navigation. This should
be contained in the NavigationTransitions class rather than the Router.
This Subject is re-exported by the router as an observable to maintain
the public API

PR Close #48257
2022-11-28 11:54:43 -08:00
Andrew Scott c20a7eeba6 refactor(router): remove malformedUriHandler from interface passed to transitions (#48257)
The malformedUriHandler is not used during the navigation transition and can be removed form the interface

PR Close #48257
2022-11-28 11:54:43 -08:00
Andrew Scott 621013af2a refactor(router): move lastSuccesfulNavigation to navigation transitions (#48257)
lastSuccessfulNavigation is only used in the transition so it does not need to live in Router

PR Close #48257
2022-11-28 11:54:43 -08:00
mgechev 864a96d7a6 docs: add state of javascript banner (#48260)
PR Close #48260
2022-11-28 11:54:18 -08:00
Kristiyan Kostadinov 458d62b7b8 build: update angular version of in-memory-web-api (#48218)
Bumps up the required Angular version of `angular-in-memory-web-api` to 15.

Fixes #48197.

PR Close #48218
2022-11-28 09:13:47 -08:00
piyush132000 ebb3f58f4c docs: removed empty methods (#48230)
I removed empty methods from a file and unnecessary import

PR Close #48230
2022-11-28 09:13:20 -08:00
Alan Agius 1e483ebe18 refactor(platform-server): remove Injector dependency for initDominoAdapter factory (#48231)
The injector is not used by this factory.

PR Close #48231
2022-11-28 09:12:36 -08:00
Alan Agius dab7198a1f refactor(core): remove unused private APIs (#48234)
Remove private code which is not used by the Framework.

PR Close #48234
2022-11-28 09:11:26 -08:00
Aristeidis Bampakos 34ccffe9ab docs: update deployment to GitHub Pages (#48239)
The GitHub Pages section has been moved to the left sidebar of the Settings page.
PR Close #48239
2022-11-28 09:11:00 -08:00
Derek Cormier cfd8702747 build(bazel): fix pwa test metrics (#48214)
Prior to the AIO bazel migration, the pwa test script only tested the
pwa metric. A bug was introduced where the score target was used for all
metrics, causing the AIO deploy test to fail.

PR Close #48214
2022-11-23 23:20:36 +00:00
Derek Cormier e56f6d2187 build(bazel): copy dist folder to aio for firebase deployment (#48214)
Firebase requires the distributable to be in the same folder as
firebase.json.

PR Close #48214
2022-11-23 23:20:36 +00:00
Derek Cormier 86fdcfcc0f build(bazel): point to correct dist folder in firebase config (#48214)
Point to the AIO dist folder in the Bazel output tree.

PR Close #48214
2022-11-23 23:20:36 +00:00
Derek Cormier 00864665a3 build(bazel): fix aio deployment unit tests (#48214)
Missing a spy for the new chmod call.

PR Close #48214
2022-11-23 23:20:36 +00:00
Derek Cormier 78b66b236b build(bazel): remove write protection on aio dist folder for deployment (#48214)
Deploy script adds extra files and needs write access.

PR Close #48214
2022-11-23 23:20:36 +00:00
Derek Cormier 049123cf6c build(bazel): fix AIO deployment unit tests (#48214)
Forgot to update the dist path in these tests.

PR Close #48214
2022-11-23 23:20:36 +00:00
Derek Cormier 7815acd125 build(bazel): fix dist dir in AIO deploy script (#48214)
The script is run cd'ed into aio so the path to the Bazel dist
location is up one folder.

PR Close #48214
2022-11-23 23:20:36 +00:00
Derek Cormier ae812e3936 build(bazel): fix AIO deployment script (#48214)
Don't run the script under Bazel, except for the tests. Configure a
Bazel flag to select the AIO build configuration.

PR Close #48214
2022-11-23 23:20:36 +00:00
Derek Cormier e044939fa9 build(bazel): fix yarn call in aio deploy script (#48214)
Incorrectly calls yarn scripts from the root package.

PR Close #48214
2022-11-23 23:20:36 +00:00
JoostK 86a21f5569 fix(compiler-cli): accept inheriting the constructor from a class in a library (#48156)
The stricter checks under `strictInjectionParameters` in Angular 15 now enforce that
an inherited constructor must be compatible with DI, based on whether all parameters
are valid injection tokens. There is an issue when the constructor is inherited from
a class in a declaration file though, as information on the usage of `@Inject()` is
not present within a declaration file. This means that this stricter check cannot be
accurately performed, resulting in false positives.

This commit disables the stricter check to behave the same as it did prior to
Angular 15, therefore avoiding the false positive.

Fixes #48152

PR Close #48156
2022-11-23 12:10:37 -08:00
Angular Robot c3ccaa83b4 build: update scorecard action dependencies (#47866)
See associated pull request for more information.

PR Close #47866
2022-11-23 10:54:13 -08:00
Angular Robot 6e9075e787 build: update bazel setup to v5.7.1 (#47884)
See associated pull request for more information.

PR Close #47884
2022-11-23 10:53:00 -08:00
Angular Robot 85c35cf693 build: update io_bazel_rules_sass digest to a2fce75 (#47970)
See associated pull request for more information.

PR Close #47970
2022-11-23 10:52:30 -08:00
Alan Agius b8ed822896 build: update safevalues to 0.4.2 (#48153)
This contains a fix which enabled the ESM version of the package to be consumed.

See: https://github.com/angular/angular/issues/48171#issuecomment-1324728758

PR Close #48153
2022-11-23 09:35:41 -08:00
Bob Watson 420af8ee47 docs: update AIO code to v15 (#48153)
PR Close #48153
2022-11-23 09:35:41 -08:00
Kristiyan Kostadinov 665d4650cd refactor(language-service): make selector nullable (#48193)
This is a follow-up from https://github.com/angular/angular/pull/48147. Changes the `DirectiveSymbol.selector` to be nullable since it's possible to have directives without a selector.

PR Close #48193
2022-11-23 09:27:03 -08:00
Dylan Hunn 40c138c13d release: cut the v15.0.1 release 2022-11-22 19:04:21 -08:00
Andrew Scott dc23ad4978 refactor(router): extract navigation transition code to a new file (#48187)
This commit extracts the Router's navigation "transitions" to a separate
file. The goal here is twofold:

- Separate the Router's logic into more manageable chunks. Rather than
  having to always edit a 1000+ line file, this separates different
  pieces into smaller, more focused files
- More importantly, this sets the groundwork for separating the Router
  state from the individual navigation transition. That is, it would be
  good for the rxjs pipeline to _not_ modify the Router state but only
  keep track of its own intermediate stages and emit outwards during
  important events. The Router can subscribe to those events and make
  updates to its state. This would theoretically allow us to swap those
  implementations independently. That is, the Router's state management
  can be changed without changing the transition handling and
  vice-versa.

PR Close #48187
2022-11-22 18:58:38 -08:00
Brooke 3596c44e0c docs: add brooke avery to GDE resources (#47923)
PR Close #47923
2022-11-22 16:22:06 -08:00
Joey Perrott 8600b3077d build: reformat BUILD files
Reformat BUILD file usage of globs.
2022-11-22 13:51:19 -07:00
Derek Cormier 31b2716c1c build(bazel): remaining AIO Bazel migration fixes
Fix the remaining AIO Bazel migration TODOs
2022-11-22 13:51:19 -07:00
Derek Cormier 65712b1d15 build(bazel): add missing dependency to deploy script
Cannot load the root package.json. Surfaced after trying to merge
aio-bazel-migration to main because the deployment short circuits
on ci for feature branhces.
2022-11-22 13:51:19 -07:00
Derek Cormier 4881e75097 build(bazel): mark a couple of aio example tests as flaky
May be caused by resource contention when running the example tests in
parallel.
2022-11-22 13:51:19 -07:00
Derek Cormier 2f5fe32b92 build(bazel): update workspace cli sources ref
Updates to the latest version still used in angular/main.
2022-11-22 13:51:19 -07:00
Derek Cormier 0136eb6d34 build(bazel): tweak parallel jobs for AIO local deps ci
Parallel runs of tests on circleci executor push RAM to the limits.
2022-11-22 13:51:19 -07:00
Derek Cormier 6efefec55c build(bazel): fix a regression when linking local packages for AIO example
tests

Caused by code moving around.
2022-11-22 13:51:19 -07:00
Derek Cormier 371a599c8e build(bazel): fix linking of local angular packages
Npm angular deps were transitively being included, confusing the
rules_nodejs linker.
2022-11-22 13:51:19 -07:00
Derek Cormier a02d114dae build(bazel): support ibazel for AIO example playgrounds
Also uses more fine-grained deps to make an ibazel rebuild fast.
2022-11-22 13:51:19 -07:00
Derek Cormier df5e1802a3 build(bazel): fix a bug where local ngc couldn't be linked into AIO example tests
The ngc entrypoint escapes the sandboxed example folder. Rather than
always symlinking local packages, copy the full package when there are
bin entries.
2022-11-22 13:51:19 -07:00
Derek Cormier c142511084 build(bazel): fix regressions for docs-watch
Some earlier refactoring prevented the watchr script from running in
the source tree. There was also a Windows issue where running architect
didn't preserve symlinks causing the app to not be served.
2022-11-22 13:51:19 -07:00
Derek Cormier 9520a029d2 build(bazel): fix remote cache missses on Windows and OSX
Caused by chromium debug logs in @angular/build-tooling.
2022-11-22 13:51:19 -07:00
Derek Cormier afad500f40 build(bazel): fix a regression where github links were incorrectly
written

Incorrectly prepended 'packages/' to docs that were under aio/content.
2022-11-22 13:51:19 -07:00
Derek Cormier 4199a8db08 build(bazel): fix missing dgeni dependencies
Fixes the 'Export has no declarations: unknown' warning and
unresolved type warnings.
2022-11-22 13:51:19 -07:00
Derek Cormier 6d9339559c build(bazel): update rnj and remove launcher patch
rules_nodejs released  a fix for the RUNFILES issue.
2022-11-22 13:51:19 -07:00
Derek Cormier 5769eb0bd2 build(bazel): consolidate windows chromium path workaround
Add the hack to a central location so that it's easier to fix later.
2022-11-22 13:51:19 -07:00
Derek Cormier da125dbb79 build(bazel): fix an AIO bug where symlinked assets were not loaded on
Windows

Added "followSymlinks" blocks on the assets.
2022-11-22 13:51:19 -07:00
Derek Cormier 84c1896b6b build(bazel): remove unused AIO package.json scripts
No longer needed after the migration
2022-11-22 13:51:19 -07:00
Derek Cormier b13783eeb6 build(bazel): update CLI sources ref for aio build
It was out of date with the current legacy build
2022-11-22 13:51:19 -07:00
Derek Cormier a908ba7a68 build(bazel): use workspace status command for AIO version stamping
Fixes a bug where the navigation map did not have the correct SHAs.
2022-11-22 13:51:19 -07:00
Derek Cormier 500acfcf98 build(bazel): create AIO example playgrounds for manual testing
After the bazel migration, AIO examples are no longer fully formed in
the source tree.
2022-11-22 13:51:19 -07:00
Derek Cormier 8df1deaad5 build(bazel): list docs entrypoints under packages to simplify docgen
target

Follows pattern in @angular/components.
2022-11-22 13:51:19 -07:00
Derek Cormier 9b169f60a6 build(bazel): fix a bug where the esm loader did not point to
node_modules in runfiles

Was passing locally previously due to escaping to the source tree.
2022-11-22 13:51:19 -07:00
Derek Cormier 71ee5d58cf build(bazel): clean up pathing in aio build tooling
Make use of process.env.RUNFILES.
2022-11-22 13:51:19 -07:00
Derek Cormier 0e0ec49f58 build(bazel): allow tag propagation on host machine
Fixes a bug where 'requires-network' tag was not propagated locally.
2022-11-22 13:51:19 -07:00
Derek Cormier 8d685f6997 build(bazel): update documentation for docs authors
Where possible, preseve the original yarn commands.
2022-11-22 13:51:19 -07:00
Derek Cormier 32cc63c067 build(bazel): fix a11y and pwa tests on Windows
Needed to use absoulte paths and fix a separator.
2022-11-22 13:51:19 -07:00
Derek Cormier eb07e5bbda build(bazel): remove old patch rules_nodejs patch
Patch contents included in rules_nodejs 5.6.0.
2022-11-22 13:51:19 -07:00
Derek Cormier 3fa6ae1ba9 build(bazel): fix a bug where $(GIT_BIN_PATH) wasn't providing a correct path on
Windows

Fixed in rules_nodejs 5.6.0
2022-11-22 13:51:19 -07:00
Derek Cormier 347e0139d5 build(bazel): fix windows regression caused by absolute esm loader path
The --loader argument can't handle absolute windows paths.
2022-11-22 13:51:19 -07:00
Derek Cormier 425035b8fd build(bazel): fix broken aio github links on Windows
Caused the aio e2e tests to fail.
2022-11-22 13:51:19 -07:00
Derek Cormier 6c6eaddbe2 build(bazel): fix aio example systemjs tests for locally linked packages
These required a preserveSymlinks hack similar to the CLI tests
2022-11-22 13:51:19 -07:00
Derek Cormier e43686f4d1 build(bazel): fix a bug where the opensearch url coudln't be substituted
The file was write protected in the bazel output tree.
2022-11-22 13:51:19 -07:00
Derek Cormier ef89eb579e build(bazel): fix a bug where the esm loader could not determine the
runfiles dir on bazel run

Was relying RUNFILES_DIR but that seems to not be available when calling
bazel run.
2022-11-22 13:51:19 -07:00
Derek Cormier 5e8159ca2a build(bazel): fix aio payload test to work with bazel output
Was pointing to legacy build dist folder.
2022-11-22 13:51:19 -07:00
Derek Cormier 6700f93183 build(bazel): fix linting errors
Exclude third_party vendored code from linting to preserve original form
2022-11-22 13:51:19 -07:00
Derek Cormier 9e435956f8 build(bazel): fix a bug where deps in aio local build were not included
This was breaking the local deps build.
2022-11-22 13:51:19 -07:00
Derek Cormier a06644b816 build(bazel): selectively enable network access for some aio targets
By default network access is disabled for actions on ci.
2022-11-22 13:51:19 -07:00
Derek Cormier bd20c71391 build(bazel): use git toolchain for dgeni build
Now using a toolchain from dev-infra that provides the
path to git in a template var.
2022-11-22 13:51:19 -07:00
Derek Cormier cc5a35123d build(bazel): fix some regressions
Caused by some incompatible changes that were merged in.
2022-11-22 13:51:19 -07:00
Derek Cormier 47045a5306 build(bazel): avoid network call during aio build by not inlining fonts
Allows the aio build target to be run under remote-exec
2022-11-22 13:51:19 -07:00
Derek Cormier f183129eab build(bazel): avoid writing ngsw-config to the source tree
This was originally done before runfiles was enabled and the output
and source trees were not merged for architect targets.
2022-11-22 13:51:19 -07:00
Derek Cormier b0c110ab3a build(bazel): fix a circular dep in aio
Probably caused by some incompatible merges.
2022-11-22 13:51:19 -07:00
Derek Cormier 19fe298d11 build(bazel): merge aio/tools/defaults.bzl and tools/defaults.bzl
Detect the correct workspace for the source maps dep.
2022-11-22 13:51:19 -07:00
Derek Cormier 57f63bf471 build(bazel): patch architect to not clear output
Otherwise a failed build requires looking in the command log for the
error.
2022-11-22 13:51:19 -07:00
Derek Cormier c0655e447a build(bazel): restore filtering for example e2e tests
Added a wrapper script that performs bazel queries, filters, then runs
the tests.
2022-11-22 13:51:19 -07:00
Derek Cormier ec286048c9 build(bazel): fix load statement to point to new dev-infra package
This was introduced during a recent rebase.
2022-11-22 13:51:19 -07:00
Derek Cormier 7c12ebb50d build(bazel): adjust aio opensearch substitution with bazel paths
Now points to the bazel dist folder.
2022-11-22 13:51:19 -07:00
Derek Cormier 4cafb67e82 build(bazel): fix a bug where aio firebase utils tests could not link
deps

ts-node doesn't appear to use the require patch
2022-11-22 13:51:19 -07:00
Derek Cormier 20b81d0647 build(bazel): run firebase deployment tests under bazel
The deployment is still run from a yarn script.
2022-11-22 13:51:19 -07:00
Derek Cormier 3603b4dbf3 build(bazel): test aio pwa score under bazel
Refactored the a11y score tests to share the pattern of running an http
server alongside a test script.
2022-11-22 13:51:19 -07:00
Derek Cormier 79554b7a39 build(bazel): run aio a11y tests under bazel
Wrap existsing scripts under bazel and write a new script
to spin up an http server on an unused port.
2022-11-22 13:51:19 -07:00
Derek Cormier 6fb3b6f5ba build(bazel): post-rebase fixes for aio bazel build
Updated cli source snapshot and had to fix up lockfile.
2022-11-22 13:51:19 -07:00
Derek Cormier 8062e7113f build(bazel): run aio firebase redirect tests under bazel
Adjust to run under ci.
2022-11-22 13:51:19 -07:00
Derek Cormier 7b5e073a70 build(bazel): run aio systemjs example e2es concurrently
These had to be run with the 'exclusive' tag under bazel due to
hardcoded ports.
2022-11-22 13:51:19 -07:00
Derek Cormier e20bfda83d build(bazel): Fix a bug where the aio build artifact didn't include assets.
Copy the applicaiton sources to the bin dir in order to find the generated relative asset paths.
2022-11-22 13:51:19 -07:00
Derek Cormier adffd3fdf7 build(bazel): build aio 404 page under bazel
The legacy build injected this file into the dist folder, however that's
not possible under bazel so files are copied to a new directory.
2022-11-22 13:51:19 -07:00
Derek Cormier d9a29e426a build(bazel): clean up manual build contents for aio example deps
This avoids the need to depend on "node_modules" and "node_modules/.bin"
separately.
2022-11-22 13:51:19 -07:00
Derek Cormier 3f92ea57ea build(bazel): fix a failing example e2e test that failed to find the
shadow root

This broke after using the chrome version provided by dev-infra.
2022-11-22 13:51:19 -07:00
Derek Cormier fa92cf055d build(bazel): fix some aio linting errors
Surfaced by modifying ci to include aio targets
2022-11-22 13:51:19 -07:00
Derek Cormier 4aab63f31e build(bazel): add aio targets to ci
This is a first pass. There is more work to do on package.json, adding
more test targets under bazel, and revising documentation.
2022-11-22 13:51:19 -07:00
Derek Cormier 8d24a91c6a build(bazel): refactor aio example e2es to fix windows performance
Use the same config flag to enable local vs npm deps as aio.
2022-11-22 13:51:19 -07:00
Derek Cormier a36034a135 build(bazel): stamp targets to build, test, and serve aio against
first party deps

Architect is not compatible with disabling the rules_nodejs linker so
these targets must use npm_link to link first party deps
2022-11-22 13:51:19 -07:00
Derek Cormier 12855f3805 build(bazel): incrementally run aio example e2e tests
Replaces the workflow where all example e2es are run at once
2022-11-22 13:51:19 -07:00
Derek Cormier f62339d1d1 build(bazel): remove rules_nodejs patch
This patch was added to remove an unnecessary wrapper script around
nodejs binaries which prevented running esm scripts when the linker
was disabled.
2022-11-22 13:51:19 -07:00
Derek Cormier d7eb429430 build(bazel): fix race conditions in windows aio build
Disables the rules_nodejs linker and adds a custom esm module resolver
2022-11-22 13:51:19 -07:00
Derek Cormier 3d6315e6e8 build(bazel): replace ignore example feature with bazel equivalent
Comment out the entry in the EXAMPLES map to ignore an example.
2022-11-22 13:51:19 -07:00
Derek Cormier 5ece26a6dc build(bazel): incrementally build aio example zips and live examples
The existing scripts were amended to work on one example at a time
2022-11-22 13:51:19 -07:00
Derek Cormier 1dc641ea30 build(bazel): fix bazel aio regressions after rebase
The aio-bazel-migration branch was rebased onto main to include runfiles
support on windows.
2022-11-22 13:51:19 -07:00
Derek Cormier 592eadd7c3 build(bazel): run aio app tests with bazel-provided chromium
Similar to the e2e protractor tests
2022-11-22 13:51:19 -07:00
Derek Cormier 216372316a build(bazel): run aio application e2e tests under bazel
Includes a postinstall fix for puppeteer so that chromium
can be run with protractor on osx under bazel
2022-11-22 13:51:19 -07:00
Derek Cormier 7cbedba3af build(bazel): create targets for aio example tooling tests
This replaces the boilerplate:test npm script
2022-11-22 13:51:19 -07:00
Derek Cormier 0b55b647aa build(bazel): create targets for aio docs tests
This replaces the docs-test npm script
2022-11-22 13:51:19 -07:00
Derek Cormier 75e623b179 build(bazel): add an aio fast serve target
This is equivalent to docs-watch in the legacy build.
2022-11-22 13:51:19 -07:00
Derek Cormier 472abd7944 build(bazel): generate aio stackblitz and example zips
Can be served by running bazel run //aio:serve
2022-11-22 13:51:19 -07:00
Derek Cormier 4ff8f27bad build(bazel): add a bazel aio serve target
This works with ibazel but is not highly incremental
2022-11-22 13:51:19 -07:00
Derek Cormier c9478f78a2 build(bazel): add bazel targets for aio doc generation
This is an incremental step to produce dgeni output with bazel. The
generated outputs are not yet used by other targets.
2022-11-22 13:51:19 -07:00
Bob Watson 8ab50a13d7 docs: fix broken link in v15 deprecations table (#48157)
PR Close #48157
2022-11-22 11:39:48 -08:00
Bob Watson f689250605 docs: add missing documentation setup instructions (#48174)
PR Close #48174
2022-11-22 11:39:25 -08:00
Wooshaah cfc224983f docs(forms): fix typos in removeValidators and hasValidator usage notes (#48144)
PR Close #48144
2022-11-22 11:39:03 -08:00
Andrew Scott ebcca47402 refactor(router): Update RouterOutlet to not read internal route property (#48112)
The `RouterOutlet` currently reads the `_futureSnapshot` of the Route.
However, by the time `activateWith` is called, this value is the same as
`snapshot` (https://github.com/angular/angular/blob/414b1b2d5ffe8eac33749bf472af7eafcf248dba/packages/router/src/operators/activate_routes.ts#L163-L205).

This change will make it easier for developers to fork the `RouterOutlet`
implementation if necessary without needing to modify any code.

PR Close #48112
2022-11-22 11:37:38 -08:00
Andrew Scott 11d916dcfd refactor(router): Remove unnecessary logic in enabledBlocking option (#48064)
The `initNavigated` flag is not necessary. The way `enabledBlocking`
works is by blocking any navigations from finishing until the
application has bootstrapped. The bootstrap is unblocked as soon as we
hit the `afterPreactivation` step. After that point,
`afterPreactivation` is always unblocked because the `bootstrapDone`
Subject is completed/stopped. There is no need for an additional
variable to track this information.

PR Close #48064
2022-11-22 11:36:14 -08:00
Andrew Scott d50fa6b79e test(router): Update flaky tests to consistently pass (#48176)
This commit makes several updates to the bootstrap tests of the Router.
The change that made the real difference was providing
`MockPlatformLocation`. Other changes include:

* using `async/await` instead of `done`
* not providing APP_BASE_HREF (recent router refactors made this unnecessary)
* using some functional guards to simplify some tests

PR Close #48176
2022-11-22 11:33:19 -08:00
Alan Agius d0631f8aa0 docs: fix dates in release guide (#48173)
A version always stays for about a year in LTS.

PR Close #48173
2022-11-22 11:32:01 -08:00
Kristiyan Kostadinov cc8b76ef7c fix(language-service): correctly handle host directive inputs/outputs (#48147)
Adds some logic to correctly handle hidden or aliased inputs/outputs in the language service.

Fixes #48102.

PR Close #48147
2022-11-22 09:47:49 -08:00
Nebojsa Cvetkovic fa5528fb5f fix(router): restore 'history.state' on popstate even if navigationId missing (#48033)
If `history.pushState()` or `history.replaceState()` were called manually without including the `navigationId` field the state was being incorrectly discarded - that logic was for maintaining the original behavior of `NavigationStart.restoredState`.

Improves on #28176, fully fixes #28108, see also #28954

PR Close #48033
2022-11-21 13:46:17 -08:00
Ikko Ashimine f612fce06f docs: fix typo in navigation.json (#48127)
documenation -> documentation
PR Close #48127
2022-11-21 13:45:51 -08:00
Andrew Scott 930af9dd26 fix(common): Fix MockPlatformLocation events and missing onPopState implementation (#48113)
The MockPlatformLocation forward, back, and historyGo methods should trigger a popstate event.
Additionally, these events should just be synchronous since that's what the majority of the major
browsers do. Lastly, onPopState should be implemented the same way as onHashChange.

PR Close #48113
2022-11-21 13:45:06 -08:00
Ivan Rodriguez 7e3dad1ad2 docs: fix typo in cli-builder.mf (#48129)
Replace `&commat;example/my-builder` with `@example/my-builder`
PR Close #48129
2022-11-21 13:44:42 -08:00
Joe Roxbury cf55aa031c docs(forms): add FormRecord class to API summary (#48136)
Add the missing FormRecord class to the
Reactive forms API summary which is located under Developer Guides ->
Forms -> Reactive Forms.

Fixes #47808

PR Close #48136
2022-11-21 13:44:17 -08:00
Alan Agius 079ec8c651 docs: add version 15 to active version (#48148)
With this change we update the releases docs to include version 15 as an active version and mark version 14 as an LTS.

Closes #48116

PR Close #48148
2022-11-21 13:43:45 -08:00
Andrew Scott 890feb9332 Revert "refactor(router): Remove promise catch and re-reject (#48067)" (#48135)
This reverts commit 07c9396824. This
change caused test failures (both unit and integration). Though the code
looks like it should be a no-op update, it's possible it affecting
something to do with microtasks, zoneJS, and change detection. More
investigation would need to be done into the exact nature of the
failures.

PR Close #48135
2022-11-21 09:24:00 -08:00
Andrew Scott a04c5387e5 Revert "Revert "refactor(router): Simplify router defaults by making the defaults Injectable" (#48121)" (#48135)
This reverts commit 57d9296802. The
original commit was not problematic and was mistakenly reverted.

PR Close #48135
2022-11-21 09:24:00 -08:00
Mikhail Savchuk bf724a257b docs: fix typo in image-directive.md (#48115)
PR Close #48115
2022-11-18 12:17:14 -08:00
Dylan Hunn e73f5e0cd8 Revert "refactor(router): Simplify router defaults by making the defaults Injectable" (#48121)
This reverts commit 18033d5ed2.

PR Close #48121
2022-11-18 12:16:17 -08:00
Greg Magolan a8c33bf931 fix(language-service): update packages/language-service/build.sh script to work with vscode-ng-language-service's new Bazel build (#48120)
Part of fix for angular/vscode-ng-language-service#1815

PR Close #48120
2022-11-18 10:21:05 -08:00
Bjarki 0c3389ecd9 docs: extract XSS security doc URL into a constant (#48082)
We plan to change the link in google3 to point to google3-specific
documentation. Replacing a single constant will make for a smaller and
more maintainable patch.

PR Close #48082
2022-11-17 13:21:16 -08:00
Volodymyr c06911b461 docs: Changed documentation for providers vs. viewProviders (#47281)
Content in ```<ng-content></ng-content>``` sees ```viewProviders``` from ```app-root```

PR Close #47281
2022-11-17 13:20:20 -08:00
Matthieu Riegler a12a120272 fix(forms): FormBuilder.group return right type with shorthand parameters. (#48084)
Extract AbstractControlOptions from type when used in shorthand parameters on FormBuilder.group

Fixes 48073

PR Close #48084
2022-11-17 11:04:54 -08:00
Andrew Scott 9baefd085f fix(router): Ensure renavigating in component init works with enabledBlocking (#48063)
The way to complete the `Subject` in a way that is able to be read on
the subject properties itself is to call `unsubscribe`:
https://github.com/ReactiveX/rxjs/blob/afac3d574323333572987e043adcd0f8d4cff546/src/internal/Subject.ts#L101-L104
This sets the `closed` property to `true` whereas `complete` does not.

fixes #48052

PR Close #48063
2022-11-17 11:01:14 -08:00
Andrew Scott 02b9d435d1 docs(router): Further document rawUrlTree (#48108)
Further document the rawUrlTree and how it's used internally in the Router.

PR Close #48108
2022-11-17 11:00:27 -08:00
Kristiyan Kostadinov b342e55509 fix(forms): don't mutate validators array (#47830)
Fixes that the `AbstractControl` was mutating the validators arrays being passed into the constructor an helper methods like `setValidators`.

Fixes #47827.

PR Close #47830
2022-11-17 09:36:14 -08:00
Matthieu Riegler c7b1cc0f3c fix(devtools): prevent devTools to load when not text/html document (#48021)
On Firefox, Angular DevTools was breaking the XML display because of the script injected
Based on contentType, we won't inject that script anymore

Fixes #48017

PR Close #48021
2022-11-17 09:35:41 -08:00
Matthieu Riegler e4309d57d8 fix(router): correct type of nextState parameter in canDeactivate (#48038)
Correct type of nextState parameter in canDeactivate guard to indicate it's never undefined

Fixes #47153

PR Close #48038
2022-11-17 09:35:03 -08:00
Pawel Kozlowski b42ff8009c refactor(core): remove usage of the useCapture arg to the listener instruction (#48046)
The useCapture argument of the listener instruction was only
meaningful in the Renderer3 case. Since the Renderer3 code was
removed this argument has no use. Note that we can perform
additional cleanup and remove the useCapture argument from the
listener instruction (this would require change to the instruction
signature, though).

PR Close #48046
2022-11-17 09:34:30 -08:00
Pawel Kozlowski 455e359763 refactor(core): remove Renderer3-related cleanup code paths (#48046)
Renderer3 had to store reference to the listener function in order to
properly cleanup event listeners. Since the Renderer3 abstraction was
removed the corresponding code path is no longer needed (Renderer2
returns a cleanup function that can be called directly).

PR Close #48046
2022-11-17 09:34:30 -08:00
George Kalpakas 5a72929d09 build(docs-infra): upgrade cli command docs sources to aa7d3b377 (#48077)
Updating [angular#15.0.x](https://github.com/angular/angular/tree/15.0.x) from
[cli-builds#15.0.x](https://github.com/angular/cli-builds/tree/15.0.x).

##
Relevant changes in
[commit range](https://github.com/angular/cli-builds/compare/50d813140...aa7d3b377):

**Modified**
- help/build.json

PR Close #48077
2022-11-17 09:31:19 -08:00
Angular Robot 2e5039bb95 build: update dependency @nguniversal/builders to v15 (#48093)
See associated pull request for more information.

PR Close #48093
2022-11-17 09:30:51 -08:00
Andrew Scott 371871a077 refactor(router): Simplify router defaults by making the defaults Injectable (#48022)
The router has several default strategy implementations that are either
assigned via `new XStrategy()` property assignements or via the factory
function for the `Router` itself. These can be simplified by providing
the default implementation in the strategy `Injectable`. Custom
implementations can still override the defaults by specifying them in
the `providers` list.

Notably, this change also makes the `UrlHandlingStrategy` and
`RouteReuseStrategy` defaults injectable via DI rather than having to
indirectly grab them by going through the `Router` instance and
accessing the properties.

PR Close #48022
2022-11-17 09:25:40 -08:00
Andrew Scott 449512020a refactor(router): remove unused currentRawUrl property from transition (#48067)
The currentRawUrl property of the transition is no longer used after various refactorings in the code over
the past couple years

PR Close #48067
2022-11-17 09:25:05 -08:00
Andrew Scott b491668207 refactor(router): Remove promise catch and re-reject (#48067)
It is not clear what the intention of the promise catch and reject is. Potentially this is
legacy code built around some ZoneJs bug that doesn't exist anymore.

PR Close #48067
2022-11-17 09:25:05 -08:00
Bob Watson 2551c78cc5 docs: add info about v15 changes and update process (#48095)
PR Close #48095
2022-11-16 16:57:43 -08:00
Jessica Janiuk bc063b2f86 docs: Add v15 release blog post link to changelog (#48094)
PR Close #48094
2022-11-16 14:36:10 -08:00
Jessica Janiuk b79ad226b2 release: cut the v15.0.0 release 2022-11-16 09:43:27 -08:00
Jeremy Elbourn bc97322641 docs: add host directives guide (#48057)
PR Close #48057
2022-11-15 20:37:00 +00:00
Jessica Janiuk 4cfb06086b release: cut the v15.0.0-rc.4 release 2022-11-14 12:05:46 -08:00
Alan Agius 933d481a0e docs: remove version 12 from actively supported versions (#48041)
Version 12 is no longer in LTS.

PR Close #48041
2022-11-14 19:28:12 +00:00
ced edea15f2c6 fix(common): export the IMAGE_CONFIG token (#48051)
The token introduced in 4fde292bb5 was not exported publicly.

PR Close #48051
2022-11-14 19:27:17 +00:00
Andrew Kushnir a1b6610c3d test(common): add e2e tests for the fill mode checks in the NgOptimizedImage directive (#48036)
This commit adds extra e2e tests for the fill mode checks in the NgOptimizedImage directive to make
sure a warning is logged in a console.

PR Close #48036
2022-11-14 09:20:40 -08:00
Alex Castle 945432e3fa fix(common): Warn on fill ngOptimizedImage without height (#48036)
Add a warning if a fill-mode image is rendered without height. This is a common occurence if the user doesn't properly set the 'position' attribute of the parent element.

PR Close #48036
2022-11-14 09:20:40 -08:00
Alex Castle 629ebad8eb docs(common): Refine NgOptimizedImage docs (#48035)
Make several changes to the structure of the documentation, based on early feedback about the readability and usefulness of the docs.

PR Close #48035
2022-11-11 15:47:04 -08:00
Andrew Kushnir 154ca82f7b docs(core): add an error details page for unsafe <iframe> bindings (#48027)
PR Close #48027
2022-11-11 11:28:35 -08:00
Mujo Osmanovic 0c0550b536 docs(docs-infra): Fix typo in documentation for standalone components (#48026)
PR Close #48026
2022-11-11 08:48:36 -08:00
Alex Castle 449d29b701 fix(common): Add fetchpriority to ngOptimizedImage preloads (#48010)
Add fetchpriority='high' to ngOptimizedImage preloads to so their priority matches the priority of the image element itself

PR Close #48010
2022-11-10 17:36:40 +00:00
Andrew Kushnir 6a9b1e11c2 release: cut the v15.0.0-rc.3 release 2022-11-09 13:40:24 -08:00
Andrew Scott 6ab9f03baf docs(router): small adjustments to router docs (#48009)
* Update extractedUrl documentation to point to the correct extract method
* Remove authService.redirectUrl from tour of heroes (it's not used anywhere)

PR Close #48009
2022-11-09 10:56:40 -08:00
Alan Agius 37a25486cf fix(core): add zone.js version 0.12.x as a valid peer dependency (#48002)
This commit adds `zone.js` version `0.12.x` as a valid peer dependency

PR Close #48002
2022-11-09 09:54:12 -08:00
Andrew Kushnir 28f289b825 fix(core): hardening attribute and property binding rules for <iframe> elements (#47964)
This commit updates the logic related to the attribute and property binding rules for <iframe> elements. There is a set of <iframe> attributes that may affect the behavior of an iframe and this change enforces that these attributes are only applied as static attributes, making sure that they are taken into account while creating an <iframe>.

If Angular detects that some of the security-sensitive attributes are applied as an attribute or property binding, it throws an error message, which contains the name of an attribute that is causing the problem and the name of a Component where an iframe is located.

BREAKING CHANGE:

Existing iframe usages may have security-sensitive attributes applied as an attribute or property binding in a template or via host bindings in a directive. Such usages would require an update to ensure compliance with the new stricter rules around iframe bindings.

PR Close #47964
2022-11-09 00:47:57 -08:00
Angular Robot 54d0981871 build: update dependency google-closure-compiler to v20221102 (#47992)
See associated pull request for more information.

PR Close #47992
2022-11-08 12:39:21 -08:00
Andrew Scott 2efcc26bb6 docs(router): Update router docs to use guard functions instead of @Injectable (#47989)
This commit updates various router docs to use the functional style
guards instead of the `@Injectable` style.

PR Close #47989
2022-11-08 11:35:08 -08:00
Alex Castle a93a1a5759 docs(common): Merge two ngOptimizedImage docs into one (#47990)
Move the old image-directive-setup.md documentation page to be a section on the main image-directive.md docs page

PR Close #47990
2022-11-08 10:36:06 -08:00
Dylan Hunn ae29f98c20 fix(forms): Runtime error pages must begin with leading zero (#47991)
I recently checked in a new error guide. The corresponding page must have a leading zero because it is a runtime error. Otherwise links to this error guide will be invalid.

PR Close #47991
2022-11-08 10:35:09 -08:00
Andrew Scott 02918c9b5a test(router): Move test for redirect under correct describe block (#47993)
This moves a redirect test to be grouped with other redirect tests.

PR Close #47993
2022-11-08 10:34:23 -08:00
Dylan Hunn 2625dc1312 fix(forms): Improve a very commonly viewed error message by adding a guide. (#47969)
[A Github issue](https://github.com/angular/angular/issues/43821) about an arcane-sounding Forms error is one of the repo's top-ten most visited pages. This converts the error to `RuntimeErrorCode` and adds a dedicated guide to explain how to solve it.

PR Close #47969
2022-11-07 16:00:06 -08:00
Kristiyan Kostadinov 3e490fab1b refactor(core): remove duplicate createRenderer call (#47981)
Fixes that we were calling `createRenderer` even though we had one already. This likely wasn't a performance issue since the renderer factory caches the renderer, but it's still better to reuse it to avoid confusion in the future.

Fixes #47980.

PR Close #47981
2022-11-07 12:33:26 -08:00
Matthieu Riegler 76b64f659c refactor(router): Simplify routerLink selector (#47941)
Since the merge of `RouterLinkWithHref` into `RouterLink` there is no need for a complex selector.

PR Close #47941
2022-11-07 12:32:43 -08:00
Andrew Scott fb7318cb86 refactor(router): Remove code to support relativeLinkResolution (#47933)
The public API for enabling this option has already been removed. This change
updates the internal Router code to not include any logic to support it.

PR Close #47933
2022-11-07 09:26:19 -08:00
Paul Gschwendtner 08ee9b4b26 build: update ng-dev configuration to account for caretaker note label change (#47987)
The caretaker note label has been renamed to the new standardized label
name. This commit updates the ng-dev caretaker check configuration to
account for this.

Also the merge assistance queue query is updated to only show PRs marked
as ready for merge. Other PRs with just a caretaker note should not be
of interest for the caretaker yet.

PR Close #47987
2022-11-07 09:25:40 -08:00
Alan Agius 46c0576efe docs: update polyfills sections for version 15 (#47975)
This commit updates the polyfills section in the browsers support guide. The `polyfills.ts` is no longer generated in version 15+

PR Close #47975
2022-11-07 09:25:04 -08:00
Albert Szekely b8a6f7491b fix(router): fix redirectTo on named outlets - resolves #33783 (#47927)
fix(router): fix redirectTo on named outlets - resolves #33783

PR Close #47927
2022-11-04 09:30:32 -07:00
Andrew Scott 8f38bbaf26 refactor(router): Warn when provideRoutes is used without provideRouter (#47896)
Due to being only 1 letter away from `provideRouter`, it is quite
possible that developers may accidentally use `provideRoutes` rather
than `provideRouter` in the `boostrapApplication` function. This change
will warn developers when `provideRoutes` is used without the `Router`.

PR Close #47896
2022-11-04 09:29:24 -07:00
Andrew Scott 80dff09134 docs(router): deprecate provideRoutes function (#47896)
This function is too similar in name to the new `provideRouter` and can
lead to misconfiguring the router. The function itself is just a small
wrapper around `{provide: ROUTES, multi: true, useValue: routes}`. It is
rarely necessary to provide routes in this way. The harm `provideRoutes`
will cause due to being only 1 letter away from `provideRouter` vastly
outweighs the benefit of a slightly shorter provider.

PR Close #47896
2022-11-04 09:29:24 -07:00
Alex Rickabaugh e7b48da713 fix(http): rename withLegacyInterceptors to withInterceptorsFromDi (#47901)
This rename reflects what the function actually does. Although the intention
is still not to have two different interceptor mechanisms, that is now
communicated in the docs for the function instead of in its name.

Fixes #47764

PR Close #47901
2022-11-03 17:54:04 -07:00
Alex Rickabaugh fe0075bdea docs(http): add public API tags and jsdoc for provideHttpClient (#47901)
This commit adds documentation and API tagging for `provideHttpClient` and
the `with*` feature functions.

PR Close #47901
2022-11-03 17:54:03 -07:00
Doug Parker 1d02e6ad68 build: add --keep_going to components tests. (#47958)
This configures CI to run all the tests and report all the failures instead of aborting after the first one.

PR Close #47958
2022-11-03 17:52:40 -07:00
Alan Agius b4706cf3e8 docs: add searchKeywords in testing guide (#47953)
This is to push up the page in search results when searching for these terms.

Ex: currently when searching using the term `test` this page is displayed at the middle of the results.

PR Close #47953
2022-11-03 17:51:59 -07:00
Peter Scriven cc4f5e6663 docs: fix lint errors for change detection guides (#47952)
PR Close #47952
2022-11-03 17:51:27 -07:00
Alan Agius f35c46a894 docs: remove CI testing setups (#47940)
This section is unnecessary given that all is needed for the user is to run `ng test`, and is also tricky to maintain (Node was hard coded to v14 still).

We have no mechanism of testing or verifying these setups, and we have no motivation to do so.

Users should see their CI providers documentation Any preconfigured Node setup is going to suggest `ng test`, which should "just work" for Angular.

PR Close #47940
2022-11-03 17:50:52 -07:00
Alan Agius f9888d928e docs: update testing document to include karma config (#47940)
In version 15, the Angular CLI will no longer generate a `karma.conf.js` by default.

This commit updates the testing document to include an example of how to include a custom `karma.conf.js`.  Also, we update several parts of the documentation which were outdated.

PR Close #47940
2022-11-03 17:50:51 -07:00
Andrew Kushnir c097c5fbcb refactor(common): improve the NgOptimizedImage error message related to changing inputs (#47926)
This commit updates the error message thrown by the NgOptimizedImage directive, when it detects a situation
when inputs change after initial rendering.

The list of inputs was also updated to include all inputs added recently.

PR Close #47926
2022-11-03 17:50:22 -07:00
Alan Agius a016508768 docs: remove redirects to no longer existing page (#47929)
This redirect is broken as this page no longer exists.

PR Close #47929
2022-11-03 17:47:55 -07:00
Markus Eckstein 164a1713d5 docs: fix missing word (#47876)
PR Close #47876
2022-11-03 17:47:28 -07:00
Alan Agius 604d89eef9 docs: update examples to reflect changes in Angular CLI version 15. (#47904)
These changes include
- Remove the usage of `enableProdMode`
- Remove `test.ts`, `polyfills.ts`, `karma.conf.js` and environment files which are no longer generated by default

PR Close #47904
2022-11-03 17:46:46 -07:00
Alan Agius 83cec3de51 docs: update analytics gathering documentation (#47840)
Currently there are 3 pages for analytics gathering information. Most of the information is duplicate.

With this change we remove the duplicate pages and outdated content to reflect the changes in version 15.

PR Close #47840
2022-11-03 17:45:35 -07:00
abergquist 7a33d395b3 docs: made code appear better, etc. (#47336)
Made code appear better and resolved some minor grammatical opportunities/issues.
PR Close #47336
2022-11-03 17:44:57 -07:00
Andrew Kushnir 8a19bc63ea release: cut the v15.0.0-rc.2 release 2022-11-03 17:32:10 -07:00
Andrew Kushnir 173dde7681 Revert "fix(core): hardening rules related to the attribute order on iframe elements (#47935)" (#47961)
This reverts commit d4b3c0b47c.

PR Close #47961
2022-11-03 17:03:43 -07:00
Andrew Kushnir d4b3c0b47c fix(core): hardening rules related to the attribute order on iframe elements (#47935)
This commit updates the logic related to the attribute order on iframes and makes the rules more strict. There is a set of iframe attributes that may affect the behavior of an iframe, this change enforces that these attributes are applied before an `src` or `srcdoc` attributes are applied to an iframe, so that they are taken into account.

If Angular detects that some of the attributes are set after the `src` or `srcdoc`, it throws an error message, which contains the name of ann attribute that is causing the problem and the name of a Component where an iframe is located. In most cases, it should be enough to change the order of attributes in a template to move the `src` or `srcdoc` ones to the very end.

BREAKING CHANGE:

Existing iframe usages may have `src` or `srcdoc` preceding other attributes. Such usages may need to be updated to ensure compliance with the new stricter rules around iframe bindings.

PR Close #47935
2022-11-02 07:23:42 -07:00
mgechev e63574774e docs: update roadmap as of q4 2022 (#47910)
PR Close #47910
2022-11-01 04:46:49 -07:00
Kristiyan Kostadinov d5cd79f885 perf(platform-browser): resolve memory leak when using animations with shadow DOM (#47903)
`AnimationRendererFactory` maintains a map between a renderer delegate and the animations renderer it corresponds to, but the renderers are never removed from the map. This leads to memory leaks when used with the `ShadowDom` view encapsulation, because the specific renderer keeps a references to its shadow root which in turn references all the elements in the view.

These changes resolve the leak by clearing the reference when the animations renderer is destroyed.

Fixes #47892.

PR Close #47903
2022-11-01 04:46:09 -07:00
Alex Rickabaugh e683298be6 docs(core): update standalone docs for provideRouter (#47902)
This commit updates the standalone components guide on AIO to showcase the
new `provideRouter` API. Previously the guide demonstrated configuring the
router via `importProvidersFrom(RouterModule.forRoot(...))`. A new section
was added to ensure `importProvidersFrom` was still shown in an example.

PR Close #47902
2022-11-01 04:45:20 -07:00
Paul Gschwendtner 99597c8c57 refactor(bazel): enable typescript strictness flags for package (#47893)
No longer maintains a separate tsconfig for the Bazel package. This
seems to be a legacy artifact back when ngc-wrapped was put into GitHub.

We now use the canonical repository Bazel tsconfig. This commit also
fixes all strictness-related errors and makes ngc-wrapped compatible.

PR Close #47893
2022-11-01 04:44:29 -07:00
Paul Gschwendtner f99cc40c5d refactor(compiler-cli): update emit signature to support for strongly typed emitCallback (#47893)
Currently `ngc-wrapped` mostly relies on any casts/or disabled
strictness checks to be able to use `tsickle`'s emit callback and
emit result merging for ngtsc. We should change this so that supertypes
of `ts.EmitResult` can be used in these optional callbacks- allowing us
to enable strictness checks in `packages/bazel/...` too.

PR Close #47893
2022-11-01 04:44:29 -07:00
Virginia Dooley 623602fc3a docs: Linting corrections. Restructuring or rewording sentences to be shorter and clearer. (#47897)
PR Close #47897
2022-10-28 02:37:47 -07:00
Alan Agius b3ea67e8a7 docs: improve grammer in CLI command template (#47891)
Before
```
This command has the following commands:
```

Now
```
This command has the following sub-commands:
```

PR Close #47891
2022-10-28 02:29:08 -07:00
Alan Agius 31a7ce1268 fix(platform-server): call onSerialize when state is empty (#47888)
Commit https://github.com/angular/angular/commit/a0b2d364156eed0d33831c37b00ea5c58ff4bbec#diff-3975e0ee5aa3e06ecbcd76f5fa5134612f7fd2e6802ca7d370973bd410aab55cR25-R31 changed the serialization phase logic so
that when the state is empty the script tag is not added to the document. As a side effect, this caused the `toJson` not called which caused the `onSerialize` callbacks also not to be called.
These callbacks are used to provide the value for a key when `toJson` is called. Example: https://github.com/ngrx/platform/issues/101#issuecomment-351998548

Closes #47172

PR Close #47888
2022-10-28 02:27:57 -07:00
Charles Lyding c7bc47c509 docs: update CLI related deprecation information (#47885)
There are no new CLI related deprecations for v15. However, multiple
features listed in the deprecation guide have since been removed and
are now also removed from the relevant sections of the guide.

PR Close #47885
2022-10-27 18:03:16 +02:00
Pawel Kozlowski 96c0e42e61 fix(core): allow readonly arrays for standalone imports (#47851)
Standalone components should support readonly arrays in the
`@Component.imports`.

Fixes #47643

PR Close #47851
2022-10-27 09:29:24 +02:00
Pawel Kozlowski f24da6baad release: cut the v15.0.0-rc.1 release 2022-10-26 20:34:19 +02:00
Andrew Scott ea7ff35b30 Revert "build: add atscott to unavailable list for pullapprove (#47658)" (#47877)
This reverts commit cf88a3ce20.

PR Close #47877
2022-10-26 19:00:55 +02:00
George Kalpakas 603194ee20 build(docs-infra): upgrade cli command docs sources to 50d813140 (#47870)
Updating [angular#15.0.x](https://github.com/angular/angular/tree/15.0.x) from
[cli-builds#15.0.x](https://github.com/angular/cli-builds/tree/15.0.x).

##
Relevant changes in
[commit range](https://github.com/angular/cli-builds/compare/46cb32b90...50d813140):

**Modified**
- help/analytics.json
- help/config.json

PR Close #47870
2022-10-26 17:35:02 +02:00
Angular Robot 5dadec56e6 build: update cross-repo angular dependencies (#47874)
See associated pull request for more information.

PR Close #47874
2022-10-26 16:33:21 +02:00
Angular Robot d75e3da429 build: update bazel setup to v5.7.0 (#47436)
See associated pull request for more information.

PR Close #47436
2022-10-26 15:43:20 +02:00
Angular Robot ebd2ddbbd1 build: update cross-repo angular dependencies (#47846)
See associated pull request for more information.

PR Close #47846
2022-10-26 14:12:24 +02:00
Angular Robot df36537bfd build: update babel dependencies (#47765)
See associated pull request for more information.

PR Close #47765
2022-10-25 21:57:13 +02:00
Angular Robot ed48887068 build: update cross-repo angular dependencies (#47871)
See associated pull request for more information.

PR Close #47871
2022-10-25 15:33:43 +02:00
Kristiyan Kostadinov 2908eba59c fix(platform-server): align server renderer interface with base renderer (#47868)
The `ServerRenderer` wasn't aligned with the `Renderer2` interface which meant that it was still referring to the old `debugInfo` parameters. It also wasn't implementing the `preserveContent` argument of `selectRootElement` which can lead to incosistencies between the server and the client.

Fixes #47844.

PR Close #47868
2022-10-25 14:51:46 +02:00
Alan Agius a8ce8f7fc4 docs: update typescript configuration (#47842)
This commits update the TypeScript configuration for the updated version 15 release.

PR Close #47842
2022-10-25 10:29:37 +02:00
Alan Agius 66eb1425e2 docs: update configuring browser compatibility (#47842)
This commits adds more information about why and how Browserslist is used in the Angular CLI.

PR Close #47842
2022-10-25 10:29:37 +02:00
Alan Agius 61f8522410 docs: update projects file structure (#47841)
With this change we update the file structure to reflect that of new projects in version 15.

PR Close #47841
2022-10-25 10:28:32 +02:00
Pawel Kozlowski c0903acbd4 Revert "fix(forms): don't mutate validators array (#47830)" (#47845)
This reverts commit 0329c13e95.

PR Close #47845
2022-10-25 10:05:18 +02:00
vyom1611 588cd3ed50 docs: removing http-server section in Service worker tour (#47613)
- Based on issue #47583
- Updated docs for Service workers by removing the section which used http-server to use service workers with ng serve
- By changelog of v14.2.0, service workers become compatible with ng serve using feature #23679

PR Close #47613
2022-10-24 15:42:57 +02:00
Kristiyan Kostadinov a99d9d67f3 fix(forms): don't mutate validators array (#47830)
Fixes that the `AbstractControl` was mutating the validators arrays being passed into the constructor an helper methods like `setValidators`.

Fixes #47827.

PR Close #47830
2022-10-24 14:12:56 +02:00
Paul Gschwendtner 6471eafde6 build: update recommended launch vscode config to use proper bazel dist path (#47831)
As of Bazel v5, the `bazel-out` symlink no longer exists. This commit
corrects the path in the VSCode recommended launch configuration. This
helps speed up this launch configuration given less files having to be
explored.

PR Close #47831
2022-10-24 10:42:22 +02:00
onrails d1840419d1 docs: fixing copy inconsistencies in schematics description (#47453)
I fixed inconsistent spelling and formatting and replaced a dead link to clarity design system starter.

docs(schematics): revert a change in schematic usage pronoun

docs: parentheses removal from schematic generation description
Co-Authored-By: Bob Watson <104218420+bob-watson@users.noreply.github.com>

PR Close #47453
2022-10-24 10:41:48 +02:00
WD Snoeijer a014ca1760 docs: fix linter errors in user-input.md (#47832)
PR Close #47832
2022-10-21 19:14:01 +02:00
Balaji fb273f6461 docs: fix minor typo in hierarchical-dependency-injection.md (#47812)
Small Typo change 
- renamed oh to or in https://angular.io/guide/hierarchical-dependency-injection#skipself-and-viewproviders
PR Close #47812
2022-10-21 17:33:12 +02:00
Alex Castle 3a18398d83 fix(common): Don't warn about image distortion is fill mode is enabled (#47824)
The image distortion warning (when rendered aspect ratio is noticeably different than intrinsic) doesn't make sense with fill mode, where the user may want the image to stretch, crop or letterbox.

PR Close #47824
2022-10-20 16:50:02 -07:00
Dylan Hunn 44c1770fc9 release: cut the v15.0.0-rc.0 release 2022-10-19 23:53:31 +02:00
Andrew Scott 79e9e8ab77 fix(router): Delay router scroll event until navigated components have rendered (#47563)
Currently, the scroll event is fired immediately after the
`NavigationEnd`. However, this is problematic because a change detection
has not been able to run, meaning that Angular will not yet have run the update
block of the component templates being rendered as part of the navigation.

This change delays the scroll event using a `setTimeout`, which will
allow Angular's change detection to run before the scroll restoration is
performed.

fixes #24547

PR Close #47563
2022-10-19 23:11:18 +02:00
Andrew Kushnir 3d5df576d1 docs: exclude lifecycle hooks of the NgOptimizedImage from docs (#47814)
This commit adds the `@nodoc` annotations to the lifecycle hooks of the `NgOptimizedImage` directive to exclude them from the docs.

PR Close #47814
2022-10-19 23:08:53 +02:00
Andrew Kushnir 074b794c50 fix(docs-infra): display "developer preview" label on class members (#47814)
This commit adds the "developer preview" label for class properties and methods.

PR Close #47814
2022-10-19 23:08:52 +02:00
Alex Castle 4f52d4e474 fix(common): don't generate srcset if noopImageLoader is used (#47804)
Do not generate a srcset if the loader being used is the default noopImageLoader. This loader does not take width into account, so it does not make sense to use it with srcsets.

PR Close #47804
2022-10-19 23:08:02 +02:00
WD Snoeijer 716352c1b0 docs: fix linter errors for accessibility.md (#47725)
PR Close #47725
2022-10-19 20:13:54 +02:00
Andrew Kushnir 6e19a12f6d docs: declare NgOptimizedImage APIs as stable (#47794)
In v14.2, we've introduced a new directive to help developers to configure images for better performance. The directive was initially released in the "developer preview" mode. We've collected the feedback, made several improvements and we are happy to announce that the NgOptimizedImage APIs are promoted to stable!

This commit updates vast majority of APIs to drop the `@developerPreview` label, which effectively documents them as stable.

There are few APIs though that retained the `@developerPreview` annotations:
- the `IMAGE_CONFIG` token
- the `ImageConfig` type
- the `fill` @Input of the directive

We plan to collect some additional feedback for the mentioned APIs and drop the `@developerPreview` annotation after that.

PR Close #47794
2022-10-19 20:13:13 +02:00
urugator 5d3bae283c docs(router): fix misleading description of BaseRouteReuseStrategy (#47786)
The original description gave a false impression that only query params and fragment changes are ignored, while actually `routeParams` changes are ignored as well.
PR Close #47786
2022-10-19 20:12:24 +02:00
Virginia Dooley 6476f158aa docs: Linting suggested corrections made. Restructuring or rewording sentences to be shorter and clearer. (#47790)
PR Close #47790
2022-10-19 20:10:56 +02:00
Kara Erickson 420d6b3054 docs(common): update image directive docs (#47796)
This is a small commit to:
- clarify that loaders aren't mandatory
- remove outdated preconnect option from docs
- clarify how width/height should be set for fixed size images
- minor structural changes for clarity

PR Close #47796
2022-10-19 15:25:14 +02:00
Virginia Dooley c2c96639e2 docs: Linting suggested corrections made. Restructuring or rewording sentences to be shorter and clearer. Inserted an introductory paragraph under level 1 heading. (#47788)
PR Close #47788
2022-10-19 15:24:12 +02:00
Kara Erickson fa4798095e fix(common): update size error to mention 'fill' mode (#47797)
The current error that is thrown when the "width" or
"height" attributes is missing doesn't mention that
"fill" mode is another option. This commit updates
the error with that option.

PR Close #47797
2022-10-19 15:23:38 +02:00
Andrew Kushnir 6f1a4b8217 refactor(common): drop deprecated selector from the NgOptimizedImage directive (#47798)
This commit updates the NgOptimizedImage directive to:
- drop a deprecated selector (the `rawSrc` one)
- drop corresponding input getter

The `rawSrc` was replaced by the `ngSrc` one during the developer preview phase.

PR Close #47798
2022-10-19 15:23:04 +02:00
Kara Erickson 23f210c0ab fix(common): warn if using supported CDN but not built-in loader (#47330)
This commit adds a missing warning if the image directive
detects that you're hosting your image on one of our
supported image CDNs but you're not using the built-in loader
for it. This excludes applications that are using a custom
loader.

PR Close #47330
2022-10-19 15:21:52 +02:00
Andrew Kushnir a2f41703d1 docs: declare Standalone APIs as stable (#47754)
In v14, we've introduced core concepts to allow Components, Directives and Pipes to configure their dependencies
 without the need to use NgModules and without the need to be declared in an NgModule. The concepts and initial
set of APIs were marked as "developer preview" to allow developers to use these APIs and share the feedback.

Since v14, we've been reviewing the entire API surface of the framework and either updating existing APIs to support standalone or creating new APIs that allowed to use Router, HttpClient and other abstractions without NgMod
ules.

Based on the mentioned work to review and stabilize APIs and also based on the positive feedback from the commun
ity, we are happy to announce that the Standalone APIs are promoted to stable!

This commit updates vast majority of standalone-related APIs to drop the `@developerPreview` label, which effect
ively documents then as stable.

Two APIs that retained the `@developerPreview` annotations are:
- withRequestsMadeViaParent (from `@angular/common/http`)
- renderApplication (from `@angular/platform-server`)

We plan to collect some additional feedback for the mentioned APIs and drop the `@developerPreview` annotation b
efore the next major release.

Co-Authored-By: Alex Rickabaugh <alx@alxandria.net>
Co-Authored-By: Andrew Scott <atscott@google.com>
Co-Authored-By: Dylan Hunn <dylhunn@gmail.com>
Co-Authored-By: Jessica Janiuk <jessicajaniuk@google.com>
Co-Authored-By: JoostK <joost.koehoorn@gmail.com>
Co-Authored-By: Kristiyan Kostadinov <crisbeto@abv.bg>
Co-Authored-By: Pawel Kozlowski <pkozlowski.opensource@gmail.com>

PR Close #47754
2022-10-17 12:15:21 +02:00
WD Snoeijer 4d61d8900d docs: fix linter errors for zone.md (#47707)
PR Close #47707
2022-10-17 12:14:45 +02:00
WD Snoeijer cf17a8096d docs: fix linter errors for web-worker.md (#47766)
PR Close #47766
2022-10-17 12:13:41 +02:00
Kristiyan Kostadinov 309b2cde51 fix(compiler-cli): implement more host directive validations as diagnostics (#47768)
Implements more of the runtime validations for host directives as compiler diagnostics so that they can be caught earlier. Also does some minor cleanup.

PR Close #47768
2022-10-17 12:12:22 +02:00
WD Snoeijer d366664007 docs: fix linter errors for view-encapsulation.md (#47772)
PR Close #47772
2022-10-17 12:11:54 +02:00
Paul Gschwendtner b9553882b8 test: update size golden for main bundle reduction (#47717)
The `main` bundle size has been reduced with the Angular CLI update.
This is not worth investigating as it's a positive side reduction likely
due to further optimizations in the Angular CLI devkit.

Also updates the other golden payload entries to capture all other CLI
related payload changes- making future payload update easier to
investigate.

PR Close #47717
2022-10-17 12:11:26 +02:00
Paul Gschwendtner 334a740ec0 build: run ng update for Angular CLI next (#47717)
Runs the Angular CLI v15 migrations for AIO.

PR Close #47717
2022-10-17 12:11:26 +02:00
Paul Gschwendtner 0a87176b74 build: update cross angular deps and lock dependencies (#47717)
Renovate no longer updated the dependencies because they were expanded
to a range. Ranges did not satisfy the `@next` tag anymore- so Renovate
did was not able to update.

This fixes the dependencies by locking them- and also updates the
versions according to our Renovate config.

PR Close #47717
2022-10-17 12:11:26 +02:00
Alan Agius b6fd814542 fix(localize): update ng add schematic to support Angular CLI version 15 (#47763)
Prior to this, the `@angular/localize/init` was added as a polyfill which caused the `@angular/localize` types not to be included in the TypeScript program which caused errors such as the below:

```
Error: src/app/app.component.ts:9:11 - error TS2304: Cannot find name '$localize'.
```

With the recent changes in the CLI (https://github.com/angular/angular-cli/pull/24032), adding `@angular/localize/init` as polyfil or in the `main.server.ts` is no longer necessary. Instead we add this as a TypeScript type. When users are running in JIT mode, we add `@angular/localize/init` as an additional entrypoint.

This change also exposes the `$localize` method as a global when importing `@angular/localize`.

Closes #47677

PR Close #47763
2022-10-17 09:58:21 +02:00
Charles Barnes 4c95484c0a docs(docs-infra): fix errors page hover styles (#47770)
fix hover styles for error messages to be consistent for dark theme

Fixes #47723

PR Close #47770
2022-10-17 09:57:47 +02:00
Paul Gschwendtner b6e9a50d22 build: update ng-dev to fix caretaker check and update label config (#47779)
* Updates ng-dev to support the new g3sync canonical JSON configs
* Updates the google-internal-tests option to support the g3 sync
  canonical JSON config
* Moves the YAML sync config to a dedicated JSON file, used by the
  action and by `yarn ng-dev`.
* Updates the ng-dev caretaker config to fix that the Merge assistance
  queue is always empty.

PR Close #47779
2022-10-17 09:57:17 +02:00
Charles Lyding 874f3fc4d3 refactor(compiler-cli): use mkdirSync recursive option instead of custom implementation (#47678)
The supported Node.js versions for the `@angular/compiler-cli` package (^14.15.0 || >=16.10.0)
allow for the use of the `recursive` option of `mkdirSync`.  Using the `recursive` option
removes the need to manually create each subdirectory in a given path.

PR Close #47678
2022-10-13 13:51:04 -07:00
abergquist 0457b58dfe docs: Separated HTML elements with blank lines (#47331)
Separated HTML elements with judicious use of space. It not only makes it easier to read the HTML; it also makes it consistent with the formatting of https://github.com/angular/angular/edit/main/aio/content/examples/what-is-angular/src/app/hello-world-bindings/hello-world-ngif.component.html.

PR Close #47331
2022-10-13 19:10:28 +00:00
Andrew Kushnir e13d97cf7e refactor(common): make provideHttpClient return EnvironmentProviders (#47758)
This commit updates the `provideHttpClient` function to return the `EnvironmentProviders` instead of a regular `Provider[]`, to make sure that the `provideHttpClient` can only be used where an environment is being setup.

PR Close #47758
2022-10-13 16:44:59 +00:00
Bob Watson 955edf2afd docs: update of documentation contrib. guide (#47381)
This update describes the content authoring and revision process in sufficient detail for technical and non-technical contributors to the angular.io documentation.

PR Close #47381
2022-10-13 16:37:26 +00:00
Paul Gschwendtner 1b48e65e89 build: wire up new github action for identifying g3-affecting PRs (#47735)
Wire up new github action for identifying g3-affecting PRs.

PR Close #47735
2022-10-13 09:43:43 +00:00
Jessica Janiuk 93cc5b2210 release: cut the v15.0.0-next.6 release 2022-10-12 11:38:19 -07:00
938 changed files with 45275 additions and 145270 deletions
+2 -31
View File
@@ -5,10 +5,10 @@
.git
node_modules
dist
aio/content
aio/node_modules
aio/content/example-playground
aio/tools/examples/shared/node_modules
packages/bazel/node_modules
aio/tools/examples/shared/example-scaffold
# All integration test node_modules folders
integration/cli-elements-universal/node_modules
@@ -38,32 +38,3 @@ integration/side-effects/node_modules
integration/terser/node_modules
integration/typings_test_ts36/node_modules
integration/typings_test_ts37/node_modules
# All integration test .yarn_local_cache folders
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/cli-hello-world-ts-47/.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
+42 -1
View File
@@ -52,10 +52,32 @@ 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 #
###############################
@@ -91,6 +113,10 @@ build:remote --remote_timeout=600
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
@@ -108,6 +134,21 @@ 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.
# https://bazel.build/reference/command-line-reference#flag--experimental_allow_tags_propagation
build --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 #
@@ -132,4 +173,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 .bazelrc.user
try-import %workspace%/.bazelrc.user
-3
View File
@@ -1,8 +1,5 @@
# 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)
+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 "xlarge" class
# Limit Bazel to consuming resources that fit in CircleCI "2xlarge+" class
# https://circleci.com/docs/2.0/configuration-reference/#resource_class
build --local_cpu_resources=20
build --local_ram_resources=32768
build --local_ram_resources=40960
# All build executed remotely should be done using our RBE configuration.
build:remote --google_default_credentials
+30 -93
View File
@@ -252,6 +252,7 @@ jobs:
- init_environment
- run: yarn -s tslint
- run: yarn -s --cwd aio lint
- 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
@@ -321,29 +322,25 @@ jobs:
webhook_url_env_var: SLACK_DEV_INFRA_CI_FAILURES_WEBHOOK_URL
test_aio:
executor: test-browser-executor
executor:
name: test-browser-executor
resource_class: 2xlarge+
steps:
- custom_attach_workspace
- init_environment
# 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
# Run all aio tests
- run:
command: yarn --cwd aio test:ci
no_output_timeout: 20m
# 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: test-browser-executor
executor:
name: test-browser-executor
resource_class: 2xlarge+
steps:
- custom_attach_workspace
- init_environment
@@ -352,74 +349,19 @@ jobs:
- run: yarn --cwd aio deploy-production
test_aio_local:
executor: test-browser-executor
steps:
- custom_attach_workspace
- init_environment
# 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
resource_class: 2xlarge+
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 all aio tests (with local Angular packages)
- 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
command: yarn --cwd aio test-local:ci
no_output_timeout: 20m
# Check the bundle sizes.
- run: yarn --cwd aio payload-size aio-local
# 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).
@@ -707,6 +649,16 @@ jobs:
- *cache_key_win
- *cache_key_win_fallback
- run:
name: Debug information for flakiness of Windows job
command: |
node -v
yarn -v
which node
which yarn
echo "Path: $PATH"
yarn node -v
# Install project dependencies, and install Bazelisk globally. This is necessary as
# Windows might error when `bazel` is invoked from the project node modules. The Bazel
# invocation might modify the symlinked project `node_modules` again, causing failures.
@@ -769,22 +721,8 @@ 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
@@ -795,7 +733,6 @@ 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
+3
View File
@@ -3,6 +3,9 @@
####################################################################################################
cp "${PROJECT_ROOT}/.circleci/bazel.windows.rc" ".bazelrc.user";
# 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.
-105
View File
@@ -1,105 +0,0 @@
#!/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);
}
+17
View File
@@ -0,0 +1,17 @@
name: 'Installing Yarn dependencies'
description: 'Installs the dependencies using Yarn'
runs:
using: 'composite'
steps:
- uses: actions/cache@9b0c1fce7a93df8e3bb8926b0d6e9d89e92f20a7 # tag=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
+2 -58
View File
@@ -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: false
disabled: true
# the name of the status
context: 'google3'
# text to show when the status is pending, {{PRNumber}} will be replaced by the PR number
@@ -32,64 +32,8 @@ 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!"
@@ -127,7 +71,7 @@ merge:
- 'ci/circleci: build'
- 'ci/circleci: lint'
- 'ci/angular: size'
- 'google3'
- 'google-internal-tests'
- 'pullapprove'
# the comment that will be added when the merge label is added despite failing checks, leave empty or set to false to disable
+42
View File
@@ -0,0 +1,42 @@
# 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@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # tag=v3.1.0
- uses: ./.github/actions/yarn-install
- uses: angular/dev-infra/github-actions/setup-bazel-remote-exec@c3339f5751f9a4fbd7d372b77e08fabc02491a80
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@c3339f5751f9a4fbd7d372b77e08fabc02491a80
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'
+45
View File
@@ -0,0 +1,45 @@
# 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:
# TODO(pgschwendtner): use a different project for framework previews here.
PREVIEW_PROJECT: ng-comp-dev
jobs:
aio-deploy:
runs-on: ubuntu-latest
if: ${{ github.event.workflow_run.conclusion == 'success' }}
steps:
- uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # tag=v3
- name: Configure Firebase deploy target
working-directory: aio/
run: |
# We can use `npx` as the Firebase deploy actions uses it too.
# The default site is used as we don't use multi-sites in the project.
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_PROJECT}}
- uses: angular/dev-infra/github-actions/deploy-previews/upload-artifacts-to-firebase@c3339f5751f9a4fbd7d372b77e08fabc02491a80
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}}'
+2 -2
View File
@@ -13,13 +13,13 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # tag=v3.1.0
- uses: angular/dev-infra/github-actions/commit-message-based-labels@c9b2ff083a50d0eac742da1e69e0cbdb98bf53dd
- uses: angular/dev-infra/github-actions/commit-message-based-labels@c3339f5751f9a4fbd7d372b77e08fabc02491a80
with:
angular-robot-key: ${{ secrets.ANGULAR_ROBOT_PRIVATE_KEY }}
post_approval_changes:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # tag=v3.1.0
- uses: angular/dev-infra/github-actions/post-approval-changes@c9b2ff083a50d0eac742da1e69e0cbdb98bf53dd
- uses: angular/dev-infra/github-actions/post-approval-changes@c3339f5751f9a4fbd7d372b77e08fabc02491a80
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@a64f3515ea822a8b228ebaab1c52e76e557a580c
- uses: angular/dev-infra/github-actions/feature-request@c3339f5751f9a4fbd7d372b77e08fabc02491a80
with:
angular-robot-key: ${{ secrets.ANGULAR_ROBOT_PRIVATE_KEY }}
@@ -0,0 +1,20 @@
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@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # tag=v3.1.0
- uses: angular/dev-infra/github-actions/google-internal-tests@c3339f5751f9a4fbd7d372b77e08fabc02491a80
with:
run-tests-guide-url: http://go/angular/g3sync
github-token: ${{ secrets.GITHUB_TOKEN }}
sync-config: ./.ng-dev/google-sync-config.json
+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/lock-closed@c9b2ff083a50d0eac742da1e69e0cbdb98bf53dd
- uses: angular/dev-infra/github-actions/lock-closed@c3339f5751f9a4fbd7d372b77e08fabc02491a80
with:
lock-bot-key: ${{ secrets.LOCK_BOT_PRIVATE_KEY }}
+3 -3
View File
@@ -30,7 +30,7 @@ jobs:
persist-credentials: false
- name: 'Run analysis'
uses: ossf/scorecard-action@e363bfca00e752f91de7b7d2a77340e2e523cb18 # tag=v2.0.4
uses: ossf/scorecard-action@99c53751e09b9529366343771cc321ec74e9bd3d # tag=v2.0.6
with:
results_file: results.sarif
results_format: sarif
@@ -39,7 +39,7 @@ jobs:
# Upload the results as artifacts.
- name: 'Upload artifact'
uses: actions/upload-artifact@3cea5372237819ed00197afe530f5a7ea3e805c8 # tag=v3.1.0
uses: actions/upload-artifact@83fd05a356d7e2593de66fc9913b3002723633cb # tag=v3.1.1
with:
name: SARIF file
path: results.sarif
@@ -47,6 +47,6 @@ jobs:
# Upload the results to GitHub's code scanning dashboard.
- name: 'Upload to code-scanning'
uses: github/codeql-action/upload-sarif@807578363a7869ca324a79039e6db9c843e0e100 # tag=v2.1.27
uses: github/codeql-action/upload-sarif@b2a92eb56d8cb930006a1c6ed86b0782dd8a4297 # tag=v2.1.35
with:
sarif_file: results.sarif
+1 -1
View File
@@ -35,7 +35,7 @@ jobs:
- 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@c9b2ff083a50d0eac742da1e69e0cbdb98bf53dd
uses: angular/dev-infra/github-actions/create-pr-for-changes@c3339f5751f9a4fbd7d372b77e08fabc02491a80
with:
branch-prefix: docs-update-events
pr-title: 'docs: update events'
+1 -1
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`.
+2 -1
View File
@@ -2,6 +2,7 @@ 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',
@@ -9,7 +10,7 @@ export const caretaker: CaretakerConfig = {
},
{
name: 'Merge Assistance Queue',
query: `is:pr is:open label:"action: merge-assistance"`,
query: `is:pr is:open label:"merge: caretaker note" label:"action: merge"`,
},
{
name: 'Initial Triage Queue',
+1
View File
@@ -8,4 +8,5 @@ export const github: GithubConfig = {
owner: 'angular',
name: 'angular',
mainBranchName: 'main',
useNgDevAuthService: true,
};
+52
View File
@@ -0,0 +1,52 @@
{
"syncedFilePatterns": ["LICENSE", "modules/benchmarks/**", "modules/system.d.ts", "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/src/ngcc/**",
"packages/compiler-cli/linker/**",
"packages/compiler-cli/ngcc/**",
"packages/compiler-cli/src/ngtsc/sourcemaps/**",
"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,9 +6,6 @@ import {PullRequestConfig} from '@angular/ng-dev';
*/
export const pullRequest: PullRequestConfig = {
githubApiMerge: false,
mergeReadyLabel: 'action: merge',
caretakerNoteLabel: 'action: caretaker note',
commitMessageFixupLabel: 'merge: fix commit message',
requiredBaseCommits: {
// PRs that target either `main` or the patch branch, need to be rebased
// on top of the latest commit message validation fix.
+30 -3
View File
@@ -61,9 +61,8 @@
version: 3
availability:
users_unavailable:
- atscott
# availability:
# users_unavailable:
# Meta field that goes unused by PullApprove to allow for defining aliases to be
# used throughout the config.
@@ -326,6 +325,7 @@ 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',
@@ -843,6 +843,7 @@ groups:
'aio/content/images/guide/setup-local/**/{*,.*}',
'aio/content/tutorial/**/{*,.*}',
'aio/content/images/guide/toh/**/{*,.*}',
'aio/content/examples/marketing-first-app/**/{*,.*}',
'aio/content/examples/toh-pt0/**/{*,.*}',
'aio/content/examples/toh-pt1/**/{*,.*}',
'aio/content/examples/toh-pt2/**/{*,.*}',
@@ -933,6 +934,8 @@ 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/update-to-version-14.md',
'aio/content/guide/update-to-version-15.md'
])
reviewers:
users:
@@ -1071,6 +1074,7 @@ 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',
@@ -1083,6 +1087,28 @@ 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:
@@ -1138,6 +1164,7 @@ groups:
'tools/bazel-repo-patches/**/{*,.*}',
'tools/circular_dependency_test/**/{*,.*}',
'tools/contributing-stats/**/{*,.*}',
'tools/esm-loader/**/{*,.*}',
'tools/gulp-tasks/**/{*,.*}',
'tools/legacy-saucelabs/**/{*,.*}',
'tools/rxjs/**/{*,.*}',
+9 -21
View File
@@ -16,7 +16,7 @@
"remoteRoot": "${workspaceRoot}",
"stopOnEntry": false,
"timeout": 600000,
"outFiles": ["${workspaceFolder}/bazel-out/**/angular/**/*.js"],
"outFiles": ["${workspaceFolder}/dist/out/**/packages/**/*.js"]
},
{
"name": "Attach to bazel test ... --config=debug (no source maps)",
@@ -30,58 +30,46 @@
"remoteRoot": "${workspaceRoot}",
"stopOnEntry": false,
"timeout": 600000,
"outFiles": ["${workspaceFolder}/bazel-out/**/angular/**/*.js"],
"outFiles": ["${workspaceFolder}/dist/out/**/packages/**/*.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}/bazel-out/**/angular/**/*.js"],
"outFiles": ["${workspaceFolder}/dist/out/**/packages/**/*.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}/bazel-out/**/angular/**/*.js"],
"outFiles": ["${workspaceFolder}/dist/out/**/packages/**/*.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}/bazel-out/**/angular/**/*.js"],
},
"outFiles": ["${workspaceFolder}/dist/out/**/packages/**/*.js"]
}
]
}
+13 -1
View File
@@ -1,4 +1,5 @@
load("//tools:defaults.bzl", "nodejs_binary")
load("//:yarn.bzl", "YARN_PATH")
package(default_visibility = ["//visibility:public"])
@@ -6,12 +7,20 @@ 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",
@@ -61,5 +70,8 @@ nodejs_binary(
name = "yarn_vendored",
data = [".yarn/releases/yarn-1.22.17.cjs"],
entry_point = ".yarn/releases/yarn-1.22.17.cjs",
visibility = ["//integration:__subpackages__"],
visibility = [
"//aio/tools/examples/shared:__pkg__",
"//integration:__subpackages__",
],
)
+182 -120
View File
@@ -1,17 +1,70 @@
<a name="14.2.6"></a>
# 14.2.6 (2022-10-12)
<a name="15.0.3"></a>
# 15.0.3 (2022-12-07)
### common
| Commit | Type | Description |
| -- | -- | -- |
| [50b1c2bf52](https://github.com/angular/angular/commit/50b1c2bf520e88828eadefcb2d0acaf151f8118e) | fix | Don't generate srcsets with very large sources ([#47997](https://github.com/angular/angular/pull/47997)) |
| [bf44dc234a](https://github.com/angular/angular/commit/bf44dc234a32069cb297ef7d3a87c2004cad8b00) | fix | Update `Location` to support base href containing `origin` ([#48327](https://github.com/angular/angular/pull/48327)) |
### compiler
| Commit | Type | Description |
| -- | -- | -- |
| [9a5d84249a](https://github.com/angular/angular/commit/9a5d84249a0fd6b5c2a77c7ab6cbf72d90c45ee3) | fix | make sure selectors inside container queries are correctly scoped ([#48353](https://github.com/angular/angular/pull/48353)) |
### compiler-cli
| Commit | Type | Description |
| -- | -- | -- |
| [3fd176a905](https://github.com/angular/angular/commit/3fd176a9054bea6f6c4cd22e8ff3b8f63b86b637) | fix | add missing period to error message ([#47744](https://github.com/angular/angular/pull/47744)) |
| [c3821f5ab5](https://github.com/angular/angular/commit/c3821f5ab5bf54b2c9d7868da79a742d4e4b7667) | perf | minimize filesystem calls when generating shims ([#47682](https://github.com/angular/angular/pull/47682)) |
| [167bc0d163](https://github.com/angular/angular/commit/167bc0d1638ffd6fe91bcb40f96c2ab90f3e01cb) | fix | Produce diagnostic rather than crash when using invalid hostDirective ([#48314](https://github.com/angular/angular/pull/48314)) |
### core
| Commit | Type | Description |
| -- | -- | -- |
| [e4dcaa513e](https://github.com/angular/angular/commit/e4dcaa513e7d5ccd3a63edf6132792873f01f7c1) | fix | unable to inject ChangeDetectorRef inside host directives ([#48355](https://github.com/angular/angular/pull/48355)) |
## Special Thanks
Alan Agius, Andrew Kushnir, Andrew Scott, Aristeidis Bampakos, Bob Watson, Charles Lyding, Joey Perrott, Joshua Morony, Mathew Berg, Paul Gschwendtner, Peter Dickten, Renan Ferro, Sri Ram, WD Snoeijer, markostanimirovic and Álvaro Martínez
Alan Agius, Alex Castle, Andrew Kushnir, Andrew Scott, Bob Watson, Derek Cormier, Joey Perrott, Konstantin Kharitonov, Kristiyan Kostadinov, Paul Gschwendtner, Pawel Kozlowski, dario-piotrowicz and piyush132000
<!-- CHANGELOG SPLIT MARKER -->
<a name="15.0.0-next.5"></a>
# 15.0.0-next.5 (2022-10-05)
<a name="15.0.2"></a>
# 15.0.2 (2022-11-30)
### compiler-cli
| Commit | Type | Description |
| -- | -- | -- |
| [86a21f5569](https://github.com/angular/angular/commit/86a21f5569bc4b8060a882bd3d542a6c002438c7) | fix | accept inheriting the constructor from a class in a library ([#48156](https://github.com/angular/angular/pull/48156)) |
## Special Thanks
Alan Agius, Andrew Scott, Aristeidis Bampakos, Bob Watson, Derek Cormier, JoostK, Kristiyan Kostadinov, Matthieu Riegler, Paul Gschwendtner, Pawel Kozlowski, Rokas Brazdžionis, mgechev and piyush132000
<!-- CHANGELOG SPLIT MARKER -->
<a name="15.0.1"></a>
# 15.0.1 (2022-11-22)
### common
| Commit | Type | Description |
| -- | -- | -- |
| [930af9dd26](https://github.com/angular/angular/commit/930af9dd2607754e778922a4bb31055123229e24) | fix | Fix MockPlatformLocation events and missing onPopState implementation ([#48113](https://github.com/angular/angular/pull/48113)) |
### forms
| Commit | Type | Description |
| -- | -- | -- |
| [b342e55509](https://github.com/angular/angular/commit/b342e5550928c14ddfb33861189f67b39e163d05) | fix | don't mutate validators array ([#47830](https://github.com/angular/angular/pull/47830)) |
| [a12a120272](https://github.com/angular/angular/commit/a12a120272024e49bd3d80d9ed1ae30dcf8622f8) | fix | FormBuilder.group return right type with shorthand parameters. ([#48084](https://github.com/angular/angular/pull/48084)) |
### language-service
| Commit | Type | Description |
| -- | -- | -- |
| [cc8b76ef7c](https://github.com/angular/angular/commit/cc8b76ef7cb908d2c95229f39bf82a13ca59570b) | fix | correctly handle host directive inputs/outputs ([#48147](https://github.com/angular/angular/pull/48147)) |
| [a8c33bf931](https://github.com/angular/angular/commit/a8c33bf93132425a617381e4aa9a6f0e8e0ddf5b) | fix | update packages/language-service/build.sh script to work with vscode-ng-language-service's new Bazel build ([#48120](https://github.com/angular/angular/pull/48120)) |
### router
| Commit | Type | Description |
| -- | -- | -- |
| [e4309d57d8](https://github.com/angular/angular/commit/e4309d57d893e111bda3744a61630d1888ddbc02) | fix | correct type of nextState parameter in canDeactivate ([#48038](https://github.com/angular/angular/pull/48038)) |
| [9baefd085f](https://github.com/angular/angular/commit/9baefd085fb079c312c4c03d79775a0fcff933b9) | fix | Ensure renavigating in component init works with enabledBlocking ([#48063](https://github.com/angular/angular/pull/48063)) |
| [fa5528fb5f](https://github.com/angular/angular/commit/fa5528fb5f0fe6e4e6ea85d39e43262018520c43) | fix | restore 'history.state' on popstate even if navigationId missing ([#48033](https://github.com/angular/angular/pull/48033)) |
## Special Thanks
Alan Agius, Andrew Scott, Bjarki, Bob Watson, Brooke, Derek Cormier, Dylan Hunn, George Kalpakas, Greg Magolan, Ikko Ashimine, Ivan Rodriguez, Jessica Janiuk, Joe Roxbury, Joey Perrott, Kristiyan Kostadinov, Matthieu Riegler, Mikhail Savchuk, Nebojsa Cvetkovic, Pawel Kozlowski, Volodymyr and Wooshaah
<!-- CHANGELOG SPLIT MARKER -->
<a name="15.0.0"></a>
# 15.0.0 (2022-11-16)
[Blog post "Angular v15 is now available"](http://goo.gle/angular-v15).
## Breaking Changes
### compiler
- Keyframes names are now prefixed with the component's "scope name".
@@ -31,6 +84,32 @@ Alan Agius, Andrew Kushnir, Andrew Scott, Aristeidis Bampakos, Bob Watson, Charl
- change the component's view encapsulation to the `None` or `ShadowDom`
- define keyframes rules in global stylesheets (e.g styles.css)
- define keyframes rules programmatically in code.
### compiler-cli
- Invalid constructors for DI may now report compilation errors
When a class inherits its constructor from a base class, the compiler may now
report an error when that constructor cannot be used for DI purposes. This may
either be because the base class is missing an Angular decorator such as
`@Injectable()` or `@Directive()`, or because the constructor contains parameters
which do not have an associated token (such as primitive types like `string`).
These situations used to behave unexpectedly at runtime, where the class may be
constructed without any of its constructor parameters, so this is now reported
as an error during compilation.
Any new errors that may be reported because of this change can be resolved either
by decorating the base class from which the constructor is inherited, or by adding
an explicit constructor to the class for which the error is reported.
- Angular compiler option `enableIvy` has been removed as Ivy is the only rendering engine.
### core
- Angular no longer supports Node.js versions `14.[15-19].x` and `16.[10-12].x`. Current supported versions of Node.js are `14.20.x`, `16.13.x` and `18.10.x`.
- TypeScript versions older than 4.8 are no longer supported.
- Existing iframe usages may have security-sensitive attributes applied as an attribute or property binding in a template or via host bindings in a directive. Such usages would require an update to ensure compliance with the new stricter rules around iframe bindings.
- Existing iframe usages may have `src` or `srcdoc` preceding other attributes. Such usages may need to be updated to ensure compliance with the new stricter rules around iframe bindings.
### forms
- setDisabledState will always be called when a `ControlValueAccessor` is attached. You can opt-out with `FormsModule.withConfig` or `ReactiveFormsModule.withConfig`.
### localize
- - `canParse` method has been removed from all translation parsers in `@angular/localize/tools`. `analyze` should be used instead.
- the `hint` parameter in the`parse` methods is now mandatory.
### router
- Previously, the `RouterOutlet` would immediately
instantiate the component being activated during navigation. Now the
@@ -38,7 +117,15 @@ Alan Agius, Andrew Kushnir, Andrew Scott, Aristeidis Bampakos, Bob Watson, Charl
could affect tests which do not trigger change detection after a router
navigation. In rarer cases, this can affect production code that relies
on the exact timing of component availability.
- The title property is now required on ActivatedRouteSnapshot
- `relativeLinkResolution` is no longer configurable in
the Router. This option was used as a means to opt out of a bug fix.
## Deprecations
### common
- The `DATE_PIPE_DEFAULT_TIMEZONE` token is now deprecated in favor
of the `DATE_PIPE_DEFAULT_OPTIONS` token, which accepts an object
as a value and the timezone can be defined as a field (called `timezone`)
on that object.
### core
- - The ability to pass an `NgModule` to the `providedIn` option for
`@Injectable` and `InjectionToken` is now deprecated.
@@ -54,29 +141,116 @@ Alan Agius, Andrew Kushnir, Andrew Scott, Aristeidis Bampakos, Bob Watson, Charl
`providedIn: 'any'` is an option with confusing semantics and is almost
never used apart from a handful of esoteric cases internal to the framework.
- The bit field signature of `Injector.get()` has been deprecated, in favor of the new options object.
- The bit field signature of `TestBed.inject()` has been deprecated, in favor of the new options object.
### router
- The `RouterLinkWithHref` directive is deprecated, use the `RouterLink` directive instead. The `RouterLink` contains the code from the `RouterLinkWithHref` to handle elements with `href` attributes.
### common
| Commit | Type | Description |
| -- | -- | -- |
| [c0c7efaf7c](https://github.com/angular/angular/commit/c0c7efaf7c8a53c1a6f137aac960757cc804f263) | feat | add `provideLocationMocks()` function to provide Location mocks ([#47674](https://github.com/angular/angular/pull/47674)) |
| [75e6297f09](https://github.com/angular/angular/commit/75e6297f0901cc98aea1626a138a820e68d026ec) | feat | add <link> preload tag on server for priority img ([#47343](https://github.com/angular/angular/pull/47343)) |
| [4fde292bb5](https://github.com/angular/angular/commit/4fde292bb58f5d5bc3cf6e634f7cff9eb0d13d84) | feat | Add automatic srcset generation to ngOptimizedImage ([#47547](https://github.com/angular/angular/pull/47547)) |
| [9483343ebf](https://github.com/angular/angular/commit/9483343ebf958297ebcb81ef313d356296a41f41) | feat | Add fill mode to NgOptimizedImage ([#47738](https://github.com/angular/angular/pull/47738)) |
| [bdb5371033](https://github.com/angular/angular/commit/bdb5371033d8e9a110619861323e8383a32d5900) | feat | add injection token for default DatePipe configuration ([#47157](https://github.com/angular/angular/pull/47157)) |
| [449d29b701](https://github.com/angular/angular/commit/449d29b701ee5a50e7279d44f4c3888a5d6f7e96) | fix | Add fetchpriority to ngOptimizedImage preloads ([#48010](https://github.com/angular/angular/pull/48010)) |
| [4f52d4e474](https://github.com/angular/angular/commit/4f52d4e47416494b4054e43a0d96383bde7813ca) | fix | don't generate srcset if noopImageLoader is used ([#47804](https://github.com/angular/angular/pull/47804)) |
| [3a18398d83](https://github.com/angular/angular/commit/3a18398d8303fb4ae1923b3a182e6abb92e3117b) | fix | Don't warn about image distortion is fill mode is enabled ([#47824](https://github.com/angular/angular/pull/47824)) |
| [edea15f2c6](https://github.com/angular/angular/commit/edea15f2c63675e86248a25649008a10e1384334) | fix | export the IMAGE_CONFIG token ([#48051](https://github.com/angular/angular/pull/48051)) |
| [8abf1c844c](https://github.com/angular/angular/commit/8abf1c844c656b41a604098889db76e8c63da720) | fix | fix formatting on oversized image error ([#47188](https://github.com/angular/angular/pull/47188)) |
| [ca7bf65933](https://github.com/angular/angular/commit/ca7bf6593380fa760891d29fba5c9f61c6e9bf8b) | fix | rename `rawSrc` -> `ngSrc` in NgOptimizedImage directive ([#47362](https://github.com/angular/angular/pull/47362)) |
| [b3879dbf14](https://github.com/angular/angular/commit/b3879dbf1470ab4f31e676f1f8909cd50d963844) | fix | support density descriptors with 2+ decimals ([#47197](https://github.com/angular/angular/pull/47197)) |
| [fa4798095e](https://github.com/angular/angular/commit/fa4798095e3820087c4a3bccc9638c5e979315da) | fix | update size error to mention 'fill' mode ([#47797](https://github.com/angular/angular/pull/47797)) |
| [23f210c0ab](https://github.com/angular/angular/commit/23f210c0abfb6104f4aa2f39d0efd096c2b7574d) | fix | warn if using supported CDN but not built-in loader ([#47330](https://github.com/angular/angular/pull/47330)) |
| [945432e3fa](https://github.com/angular/angular/commit/945432e3fa2cb22ff911eda2a8ad3302a8adba5a) | fix | Warn on fill ngOptimizedImage without height ([#48036](https://github.com/angular/angular/pull/48036)) |
### compiler
| Commit | Type | Description |
| -- | -- | -- |
| [051f75648d](https://github.com/angular/angular/commit/051f75648d6065949796ac1c7ea67e71e31b011e) | fix | scope css keyframes in emulated view encapsulation ([#42608](https://github.com/angular/angular/pull/42608)) |
| [39b72e208b](https://github.com/angular/angular/commit/39b72e208b46d80f1d9a802cebf043c2ccf3c5f2) | fix | update element schema ([#47552](https://github.com/angular/angular/pull/47552)) |
| [48b354a83e](https://github.com/angular/angular/commit/48b354a83e6d94735a03eebb3a52c5698e7a0f44) | fix | update element schema ([#47552](https://github.com/angular/angular/pull/47552)) |
### compiler-cli
| Commit | Type | Description |
| -- | -- | -- |
| [bc54687c7b](https://github.com/angular/angular/commit/bc54687c7b91efe451aa744d2d3a15ca3524231e) | fix | exclude abstract classes from `strictInjectionParameters` requirement ([#44615](https://github.com/angular/angular/pull/44615)) |
| [309b2cde51](https://github.com/angular/angular/commit/309b2cde51d1577d6561e826be01e2b5fce43c49) | fix | implement more host directive validations as diagnostics ([#47768](https://github.com/angular/angular/pull/47768)) |
| [2e1dddec45](https://github.com/angular/angular/commit/2e1dddec45fef8291b1f3abce2a937e28bb75a87) | fix | support hasInvalidatedResolutions. ([#47585](https://github.com/angular/angular/pull/47585)) |
| [19ad4987f9](https://github.com/angular/angular/commit/19ad4987f9070222bb2fb8bd07a43ed7995f602a) | fix | use @ts-ignore. ([#47636](https://github.com/angular/angular/pull/47636)) |
| [8fcadaad48](https://github.com/angular/angular/commit/8fcadaad48b2b1328f47b7603b230445a26f95a8) | perf | cache source file for reporting type-checking diagnostics ([#47471](https://github.com/angular/angular/pull/47471)) |
| [16f96eeabf](https://github.com/angular/angular/commit/16f96eeabf77964092b4b6a830e29f2761ffaeec) | refactor | remove `enableIvy` options ([#47346](https://github.com/angular/angular/pull/47346)) |
### core
| Commit | Type | Description |
| -- | -- | -- |
| [e3cef4a784](https://github.com/angular/angular/commit/e3cef4a7843d22fd004f1e27afcc42d0fbbef74a) | docs | deprecate `providedIn: NgModule` and `providedIn: 'any'` ([#47616](https://github.com/angular/angular/pull/47616)) |
| [1b9fd46d14](https://github.com/angular/angular/commit/1b9fd46d14ed46f78b6d761b3917ded1f0d51e0d) | feat | add support for Node.js version 18 ([#47730](https://github.com/angular/angular/pull/47730)) |
| [ed11a13c3c](https://github.com/angular/angular/commit/ed11a13c3c5cccb0094c1a4ff9c60ea741a42067) | feat | drop support for TypeScript 4.6 and 4.7 ([#47690](https://github.com/angular/angular/pull/47690)) |
| [db28badfe6](https://github.com/angular/angular/commit/db28badfe67f404e81dacd7c3131af105feb3412) | feat | enable the new directive composition API ([#47642](https://github.com/angular/angular/pull/47642)) |
| [7de1469be6](https://github.com/angular/angular/commit/7de1469be62b08037681ee9e75614fb800700ef8) | feat | introduce EnvironmentProviders wrapper type ([#47669](https://github.com/angular/angular/pull/47669)) |
| [841c8e5138](https://github.com/angular/angular/commit/841c8e51386645d7fd26642f41952ed8f0a2dbe5) | feat | support object-based DI flags in Injector.get() ([#46761](https://github.com/angular/angular/pull/46761)) |
| [120555a626](https://github.com/angular/angular/commit/120555a626d66523e46ce01681b11a0f38e3a40a) | feat | support object-based DI flags in TestBed.inject() ([#46761](https://github.com/angular/angular/pull/46761)) |
| [96c0e42e61](https://github.com/angular/angular/commit/96c0e42e61b8a4fced1354da0162c06e7b029cf3) | fix | allow readonly arrays for standalone imports ([#47851](https://github.com/angular/angular/pull/47851)) |
| [28f289b825](https://github.com/angular/angular/commit/28f289b825be7f4bf6dc5db69197741867b8ea23) | fix | hardening attribute and property binding rules for <iframe> elements ([#47964](https://github.com/angular/angular/pull/47964)) |
| [d4b3c0b47c](https://github.com/angular/angular/commit/d4b3c0b47c32a9a664d3073164d04c0385058008) | fix | hardening rules related to the attribute order on iframe elements ([#47935](https://github.com/angular/angular/pull/47935)) |
| [85330f3fd9](https://github.com/angular/angular/commit/85330f3fd9ac6381ce3aa18479ed8195d2ac215e) | fix | update `isDevMode` to rely on `ngDevMode` ([#47475](https://github.com/angular/angular/pull/47475)) |
### forms
| Commit | Type | Description |
| -- | -- | -- |
| [a8569e3802](https://github.com/angular/angular/commit/a8569e38029724a7d77935bccce99117a7e1aefa) | feat | export forms utility functions: isFormArray, isFormGroup… ([#47718](https://github.com/angular/angular/pull/47718)) |
| [96b7fe93af](https://github.com/angular/angular/commit/96b7fe93af361a1cf2ea5477970f64ba6f3d8cd5) | fix | call `setDisabledState` on `ControlValueAcessor` when control is enabled ([#47576](https://github.com/angular/angular/pull/47576)) |
| [a99d9d67f3](https://github.com/angular/angular/commit/a99d9d67f382c18b46c4c1c6765bbda445ca0b8c) | fix | don't mutate validators array ([#47830](https://github.com/angular/angular/pull/47830)) |
| [2625dc1312](https://github.com/angular/angular/commit/2625dc13127da9f8d5fa79f1b32ad132d6656c63) | fix | Improve a very commonly viewed error message by adding a guide. ([#47969](https://github.com/angular/angular/pull/47969)) |
| [ae29f98c20](https://github.com/angular/angular/commit/ae29f98c20390abbf84d2df312ee5e8766195d60) | fix | Runtime error pages must begin with leading zero ([#47991](https://github.com/angular/angular/pull/47991)) |
### http
| Commit | Type | Description |
| -- | -- | -- |
| [3ba99e286a](https://github.com/angular/angular/commit/3ba99e286a166d122fb334171b6a9a1e6461a724) | feat | allow for child `HttpClient`s to request via parents ([#47502](https://github.com/angular/angular/pull/47502)) |
| [84d0d33c35](https://github.com/angular/angular/commit/84d0d33c3537f0da927e0ce449ede78ef9bebc6b) | feat | introduce `provideHttpClientTesting` provider function ([#47502](https://github.com/angular/angular/pull/47502)) |
| [62c7a7a16e](https://github.com/angular/angular/commit/62c7a7a16e035c3a5346270dc4c5c6de85bf9137) | feat | introduce functional interceptors ([#47502](https://github.com/angular/angular/pull/47502)) |
| [e47b129070](https://github.com/angular/angular/commit/e47b129070655f2e9eeac58bc2d0ea5648f41045) | feat | introduce the `provideHttpClient()` API ([#47502](https://github.com/angular/angular/pull/47502)) |
| [ea16a98dfe](https://github.com/angular/angular/commit/ea16a98dfef0de33c192e328f151cca39749a488) | fix | better handle unexpected `undefined` XSRF tokens ([#47683](https://github.com/angular/angular/pull/47683)) |
| [e7b48da713](https://github.com/angular/angular/commit/e7b48da713f32c02c096f1342ab8b0d7ec696ca5) | fix | rename `withLegacyInterceptors` to `withInterceptorsFromDi` ([#47901](https://github.com/angular/angular/pull/47901)) |
### language-service
| Commit | Type | Description |
| -- | -- | -- |
| [bebef5fb43](https://github.com/angular/angular/commit/bebef5fb43dcf54a109499d0d9dd701786c33f60) | feat | Quick fix to import a component when its selector is used ([#47088](https://github.com/angular/angular/pull/47088)) |
| [e7ee53c541](https://github.com/angular/angular/commit/e7ee53c541da0a1f85c217354ec9901010ae0de9) | feat | support to fix invalid banana in box ([#47393](https://github.com/angular/angular/pull/47393)) |
### localize
| Commit | Type | Description |
| -- | -- | -- |
| [400a6b5e37](https://github.com/angular/angular/commit/400a6b5e3707f3939d84c659a115b75ef15d2c09) | fix | add polyfill in polyfills array instead of polyfills.ts ([#47569](https://github.com/angular/angular/pull/47569)) |
| [b6fd814542](https://github.com/angular/angular/commit/b6fd81454207bd256a48d5726f3bd7e6ce6a489b) | fix | update ng add schematic to support Angular CLI version 15 ([#47763](https://github.com/angular/angular/pull/47763)) |
| [d36fd3d9e4](https://github.com/angular/angular/commit/d36fd3d9e41984818af5e1aad5e3004574d837bb) | refactor | remove deprecated `canParse` method from TranslationParsers ([#47275](https://github.com/angular/angular/pull/47275)) |
### platform-server
| Commit | Type | Description |
| -- | -- | -- |
| [2908eba59c](https://github.com/angular/angular/commit/2908eba59c25e9a1fc5aa257b4a8a247db82079e) | fix | align server renderer interface with base renderer ([#47868](https://github.com/angular/angular/pull/47868)) |
### router
| Commit | Type | Description |
| -- | -- | -- |
| [7bee28d037](https://github.com/angular/angular/commit/7bee28d037a8a21a7440293b3e8c118cc93ec8c1) | feat | add a migration to remove `relativeLinkResolution` usages ([#47604](https://github.com/angular/angular/pull/47604)) |
| [5163e3d876](https://github.com/angular/angular/commit/5163e3d876cdfd6d309d7e35aefa6f07ef62715e) | feat | Add UrlTree constructor to public API ([#47186](https://github.com/angular/angular/pull/47186)) |
| [da58801f95](https://github.com/angular/angular/commit/da58801f95c66c201e332189af25702bdd722f3f) | feat | auto-unwrap default exports when lazy loading ([#47586](https://github.com/angular/angular/pull/47586)) |
| [c3f857975d](https://github.com/angular/angular/commit/c3f857975d56cac6ad3939d64f76a51455159c23) | feat | make RouterOutlet name an Input so it can be set dynamically ([#46569](https://github.com/angular/angular/pull/46569)) |
| [f73ef21442](https://github.com/angular/angular/commit/f73ef2144279b6b26902510f90ca1fccb8e166b2) | feat | merge `RouterLinkWithHref` into `RouterLink` ([#47630](https://github.com/angular/angular/pull/47630)) |
| [16c8f55663](https://github.com/angular/angular/commit/16c8f55663c30270fcd647b1a8a20ddbc8923349) | feat | migrate `RouterLinkWithHref` references to `RouterLink` ([#47599](https://github.com/angular/angular/pull/47599)) |
| [07017a7bd3](https://github.com/angular/angular/commit/07017a7bd30c6bb7bd1b94a9fd1b112ee68c9ced) | feat | prevent `provideRouter()` from usage in @Component ([#47669](https://github.com/angular/angular/pull/47669)) |
| [79e9e8ab77](https://github.com/angular/angular/commit/79e9e8ab779d230f6a1df25c4ccff94b13129305) | fix | Delay router scroll event until navigated components have rendered ([#47563](https://github.com/angular/angular/pull/47563)) |
| [6a88bad019](https://github.com/angular/angular/commit/6a88bad0192516f26a5a008c0634b73456b9447c) | fix | Ensure ActivatedRouteSnapshot#title has correct value ([#47481](https://github.com/angular/angular/pull/47481)) |
| [7b89d95c0e](https://github.com/angular/angular/commit/7b89d95c0e7370d33f006aba8e67bafb53a2fd4f) | fix | Remove deprecated relativeLinkResolution ([#47623](https://github.com/angular/angular/pull/47623)) |
## Special Thanks
Alan Agius, Alex Rickabaugh, Alexander Wiebe, Andrew Kushnir, Andrew Scott, Ciprian Sauliuc, Dmytro Mezhenskyi, Dylan Hunn, George Kalpakas, Joe Martin (Crowdstaffing), Jordan, Kristiyan Kostadinov, Martin Probst, Ole M, Paul Gschwendtner, Pawel Kozlowski, dario-piotrowicz and mgechev
Alan Agius, AleksanderBodurri, Alex Castle, Alex Rickabaugh, Andrew Kushnir, Andrew Scott, Charles Lyding, Dylan Hunn, Ferdinand Malcher, George Kalpakas, Jeremy Elbourn, Jessica Janiuk, JiaLiPassion, Joey Perrott, JoostK, Kara Erickson, Kristiyan Kostadinov, Martin Probst, Matthias Weiß, Matthieu Riegler, Paul Gschwendtner, Pawel Kozlowski, Sabareesh Kappagantu, WD Snoeijer, angular-robot[bot], arturovt, ced, dario-piotrowicz, ivanwonder and jaybell
<!-- CHANGELOG SPLIT MARKER -->
<a name="14.2.6"></a>
# 14.2.6 (2022-10-12)
### compiler-cli
| Commit | Type | Description |
| -- | -- | -- |
| [3fd176a905](https://github.com/angular/angular/commit/3fd176a9054bea6f6c4cd22e8ff3b8f63b86b637) | fix | add missing period to error message ([#47744](https://github.com/angular/angular/pull/47744)) |
| [c3821f5ab5](https://github.com/angular/angular/commit/c3821f5ab5bf54b2c9d7868da79a742d4e4b7667) | perf | minimize filesystem calls when generating shims ([#47682](https://github.com/angular/angular/pull/47682)) |
## Special Thanks
Alan Agius, Andrew Kushnir, Andrew Scott, Aristeidis Bampakos, Bob Watson, Charles Lyding, Joey Perrott, Joshua Morony, Mathew Berg, Paul Gschwendtner, Peter Dickten, Renan Ferro, Sri Ram, WD Snoeijer, markostanimirovic and Álvaro Martínez
<!-- CHANGELOG SPLIT MARKER -->
@@ -90,35 +264,6 @@ Alexander Wiebe, Ciprian Sauliuc, Dmytro Mezhenskyi, George Kalpakas, Joe Martin
<!-- CHANGELOG SPLIT MARKER -->
<a name="15.0.0-next.4"></a>
# 15.0.0-next.4 (2022-09-28)
## Breaking Changes
### router
- The title property is now required on ActivatedRouteSnapshot
## Deprecations
### core
- The bit field signature of `Injector.get()` has been deprecated, in favor of the new options object.
- The bit field signature of `TestBed.inject()` has been deprecated, in favor of the new options object.
### core
| Commit | Type | Description |
| -- | -- | -- |
| [841c8e5138](https://github.com/angular/angular/commit/841c8e51386645d7fd26642f41952ed8f0a2dbe5) | feat | support object-based DI flags in Injector.get() ([#46761](https://github.com/angular/angular/pull/46761)) |
| [120555a626](https://github.com/angular/angular/commit/120555a626d66523e46ce01681b11a0f38e3a40a) | feat | support object-based DI flags in TestBed.inject() ([#46761](https://github.com/angular/angular/pull/46761)) |
| [aef8d0bb02](https://github.com/angular/angular/commit/aef8d0bb020931e0b44e067b074a449fc66f6ff7) | fix | correctly check for `typeof` of undefined in `ngDevMode` check ([#47480](https://github.com/angular/angular/pull/47480)) |
| [85330f3fd9](https://github.com/angular/angular/commit/85330f3fd9ac6381ce3aa18479ed8195d2ac215e) | fix | update `isDevMode` to rely on `ngDevMode` ([#47475](https://github.com/angular/angular/pull/47475)) |
### language-service
| Commit | Type | Description |
| -- | -- | -- |
| [e7ee53c541](https://github.com/angular/angular/commit/e7ee53c541da0a1f85c217354ec9901010ae0de9) | feat | support to fix invalid banana in box ([#47393](https://github.com/angular/angular/pull/47393)) |
### router
| Commit | Type | Description |
| -- | -- | -- |
| [6a88bad019](https://github.com/angular/angular/commit/6a88bad0192516f26a5a008c0634b73456b9447c) | fix | Ensure ActivatedRouteSnapshot#title has correct value ([#47481](https://github.com/angular/angular/pull/47481)) |
## Special Thanks
Alan Agius, Alex Rickabaugh, Andrew Kushnir, Andrew Scott, Ashley Hunter, Doug Parker, Jessica Janiuk, JiaLiPassion, Kristiyan Kostadinov, Rokas Brazdžionis, Simona Cotin and ivanwonder
<!-- CHANGELOG SPLIT MARKER -->
<a name="14.2.4"></a>
# 14.2.4 (2022-09-28)
### compiler-cli
@@ -134,21 +279,6 @@ Alan Agius, Ashley Hunter, Doug Parker, Jessica Janiuk, JoostK, Kristiyan Kostad
<!-- CHANGELOG SPLIT MARKER -->
<a name="15.0.0-next.3"></a>
# 15.0.0-next.3 (2022-09-21)
### animations
| Commit | Type | Description |
| -- | -- | -- |
| [d11b1d99b0](https://github.com/angular/angular/commit/d11b1d99b0af1bc6389ef0e0157c5bca8a7f951a) | fix | make sure that the useAnimation function delay is applied ([#47468](https://github.com/angular/angular/pull/47468)) |
### compiler-cli
| Commit | Type | Description |
| -- | -- | -- |
| [8fcadaad48](https://github.com/angular/angular/commit/8fcadaad48b2b1328f47b7603b230445a26f95a8) | perf | cache source file for reporting type-checking diagnostics ([#47471](https://github.com/angular/angular/pull/47471)) |
## Special Thanks
AleksanderBodurri, Andrew Kushnir, Andrew Scott, Bob Watson, George Kalpakas, JiaLiPassion, Joey Perrott, JoostK, Kristiyan Kostadinov, Mauro Mattos, Pawel Kozlowski, Sabareesh Kappagantu, angular-robot[bot], dario-piotrowicz, fabioemoutinho and famzila
<!-- CHANGELOG SPLIT MARKER -->
<a name="14.2.3"></a>
# 14.2.3 (2022-09-21)
### animations
@@ -160,29 +290,6 @@ AleksanderBodurri, Andrew Kushnir, Andrew Scott, Bob Watson, George Kalpakas, Jo
<!-- CHANGELOG SPLIT MARKER -->
<a name="15.0.0-next.2"></a>
# 15.0.0-next.2 (2022-09-14)
### animations
| Commit | Type | Description |
| -- | -- | -- |
| [7ec196e0fb](https://github.com/angular/angular/commit/7ec196e0fb7d73caada1f502758cab0950e3a272) | fix | make sure that the animation function delay is applied ([#47285](https://github.com/angular/angular/pull/47285)) |
### common
| Commit | Type | Description |
| -- | -- | -- |
| [ca7bf65933](https://github.com/angular/angular/commit/ca7bf6593380fa760891d29fba5c9f61c6e9bf8b) | fix | rename `rawSrc` -> `ngSrc` in NgOptimizedImage directive ([#47362](https://github.com/angular/angular/pull/47362)) |
### core
| Commit | Type | Description |
| -- | -- | -- |
| [4e10a74941](https://github.com/angular/angular/commit/4e10a7494130b9bb4772ee8f76b66675867b2145) | fix | imply @Optional flag when a default value is provided ([#47242](https://github.com/angular/angular/pull/47242)) |
### forms
| Commit | Type | Description |
| -- | -- | -- |
| [4a13210ecd](https://github.com/angular/angular/commit/4a13210ecd692aaace7f67fadf25741f3f0cc50b) | fix | don't prevent default behavior for forms with method="dialog" ([#47308](https://github.com/angular/angular/pull/47308)) |
## Special Thanks
Abhishek Rawat, Andrew Kushnir, Benjamin Chanudet, Bob Watson, Dylan Hunn, George Kalpakas, Ikko Ashimine, Joey Perrott, Kristiyan Kostadinov, Marc Wrobel, Mariia Subkov, Pawel Kozlowski, Sebastian, abergquist, dario-piotrowicz, onrails and vyom1611
<!-- CHANGELOG SPLIT MARKER -->
<a name="14.2.2"></a>
# 14.2.2 (2022-09-14)
### animations
@@ -206,41 +313,6 @@ Abhishek Rawat, Andrew Kushnir, Benjamin Chanudet, Bob Watson, George Kalpakas,
<!-- CHANGELOG SPLIT MARKER -->
<a name="15.0.0-next.1"></a>
# 15.0.0-next.1 (2022-09-07)
## Breaking Changes
### compiler-cli
- Angular compiler option `enableIvy` has been removed as Ivy is the only rendering engine.
### localize
- - `canParse` method has been removed from all translation parsers in `@angular/localize/tools`. `analyze` should be used instead.
- the `hint` parameter in the`parse` methods is now mandatory.
### common
| Commit | Type | Description |
| -- | -- | -- |
| [220d1203e6](https://github.com/angular/angular/commit/220d1203e66116bfcf69204ebac2092eb638bb75) | fix | improve formatting of image warnings ([#47299](https://github.com/angular/angular/pull/47299)) |
| [93d7d9101e](https://github.com/angular/angular/commit/93d7d9101e56a51f1ce315b1d0e67fa02e8916d9) | fix | use DOCUMENT token to query for preconnect links ([#47353](https://github.com/angular/angular/pull/47353)) |
### compiler
| Commit | Type | Description |
| -- | -- | -- |
| [cf0c53aa1c](https://github.com/angular/angular/commit/cf0c53aa1c54bf747529a9f7899e68c71249fc9d) | fix | avoid errors for inputs with Object-builtin names ([#47220](https://github.com/angular/angular/pull/47220)) |
### compiler-cli
| Commit | Type | Description |
| -- | -- | -- |
| [16f96eeabf](https://github.com/angular/angular/commit/16f96eeabf77964092b4b6a830e29f2761ffaeec) | refactor | remove `enableIvy` options ([#47346](https://github.com/angular/angular/pull/47346)) |
### localize
| Commit | Type | Description |
| -- | -- | -- |
| [d36fd3d9e4](https://github.com/angular/angular/commit/d36fd3d9e41984818af5e1aad5e3004574d837bb) | refactor | remove deprecated `canParse` method from TranslationParsers ([#47275](https://github.com/angular/angular/pull/47275)) |
### service-worker
| Commit | Type | Description |
| -- | -- | -- |
| [e1bb638e0b](https://github.com/angular/angular/commit/e1bb638e0bc9cf2da4ba3aa8ad7b549164ad5c12) | fix | include headers in requests for assets ([#47260](https://github.com/angular/angular/pull/47260)) |
| [a0c3326485](https://github.com/angular/angular/commit/a0c3326485548e8d9b193f5472a9b02a02d679a6) | fix | only consider GET requests as navigation requests ([#47263](https://github.com/angular/angular/pull/47263)) |
## Special Thanks
Alan Agius, Andrew Kushnir, Aristeidis Bampakos, Asaf M, Bingo's Code, Bob Watson, Daniel Ostrovsky, George Kalpakas, Giovanni Alberto Rivas, Jeremy Elbourn, JiaLiPassion, Jobayer Hossain, Joe Martin (Crowdstaffing), Joey Perrott, JoostK, Kara Erickson, Kristiyan Kostadinov, Maina Wycliffe, Sabareesh Kappagantu, Simona Cotin, Sonu Sindhu, Yann Provoost, abergquist, angular-robot[bot], jaybell and vyom1611
<!-- CHANGELOG SPLIT MARKER -->
<a name="14.2.1"></a>
# 14.2.1 (2022-09-07)
### common
@@ -262,16 +334,6 @@ Aristeidis Bampakos, Asaf M, Bingo's Code, Bob Watson, Daniel Ostrovsky, George
<!-- CHANGELOG SPLIT MARKER -->
<a name="15.0.0-next.0"></a>
### router
| Commit | Type | Description |
| -- | -- | -- |
| [5163e3d876](https://github.com/angular/angular/commit/5163e3d876cdfd6d309d7e35aefa6f07ef62715e) | feat | Add UrlTree constructor to public API ([#47186](https://github.com/angular/angular/pull/47186)) |
## Special Thanks
Alan Agius, Alex Rickabaugh, Andrew Scott, Joey Perrott, Kara Erickson, Kristiyan Kostadinov and Pawel Kozlowski
<!-- CHANGELOG SPLIT MARKER -->
<a name="14.2.0"></a>
# 14.2.0 (2022-08-25)
### animations
+77 -13
View File
@@ -7,6 +7,7 @@ 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
@@ -24,8 +25,12 @@ http_archive(
http_archive(
name = "build_bazel_rules_nodejs",
sha256 = "c78216f5be5d451a42275b0b7dc809fb9347e2b04a68f68bad620a2b01f5c774",
urls = ["https://github.com/bazelbuild/rules_nodejs/releases/download/5.5.2/rules_nodejs-5.5.2.tar.gz"],
patches = [
# TODO(devversion): remove when https://github.com/bazelbuild/rules_nodejs/pull/3605 is available.
"//tools:bazel-repo-patches/rules_nodejs__#3605.patch",
],
sha256 = "c29944ba9b0b430aadcaf3bf2570fece6fc5ebfb76df145c6cdad40d65c20811",
urls = ["https://github.com/bazelbuild/rules_nodejs/releases/download/5.7.0/rules_nodejs-5.7.0.tar.gz"],
)
load("@build_bazel_rules_nodejs//:repositories.bzl", "build_bazel_rules_nodejs_dependencies")
@@ -46,9 +51,35 @@ http_archive(
# Fetch Aspect lib for utilities like write_source_files
http_archive(
name = "aspect_bazel_lib",
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",
sha256 = "be236556c7b9c7b91cb370e837fdcec62b6e8893408cd4465ae883c9d7c67024",
strip_prefix = "bazel-lib-1.18.0",
url = "https://github.com/aspect-build/bazel-lib/archive/refs/tags/v1.18.0.tar.gz",
)
# Download cli source from angular/cli-builds for doc generation at a specific tag or commit.
# If the ref is a commit sha, use the full sha instead of the abbreviated form. Tags, which
# hold an abbreviated sha by convention in cli-builds, can continue to use the short form.
CLI_SRC_REF = "6ccbc6b4cba4fde7898b67d2e195a1ea3bf4553e"
http_archive(
name = "angular_cli_src",
build_file_content = """
# Include files used in doc generation
filegroup(
name = "files_for_docgen",
srcs = glob([
"help/**/*.json",
"package.json",
]),
visibility = ["//visibility:public"],
)
""",
# Run the following command to calculate the sha256, substituting the CLI_SRC_REF
# wget -O- -q https://github.com/angular/cli-builds/archive/{CLI_SRC_REF}.tar.gz | sha256sum
# Alternatively, just remove the parameter and bazel debug will output the sha as a suggestion.
sha256 = "6558e1774904719d0615f01b88368d7fe3a927ea1e54cb0436e7957bc8f6d0a6",
strip_prefix = "cli-builds-%s" % CLI_SRC_REF,
url = "https://github.com/angular/cli-builds/archive/%s.tar.gz" % CLI_SRC_REF,
)
# Setup the Node.js toolchain.
@@ -68,7 +99,7 @@ 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/releases/yarn-1.22.17.cjs",
YARN_LABEL,
"//:.yarnrc",
"//:scripts/puppeteer-chromedriver-versions.js",
"//:scripts/webdriver-manager-update.js",
@@ -83,7 +114,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/releases/yarn-1.22.17.cjs",
yarn = YARN_LABEL,
yarn_lock = "//:yarn.lock",
)
@@ -92,8 +123,9 @@ 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/releases/yarn-1.22.17.cjs",
YARN_LABEL,
"//:.yarnrc",
"//aio:tools/cli-patches/bazel-architect-output.patch",
"//aio:tools/cli-patches/patch.js",
],
# Currently disabled due to:
@@ -105,10 +137,34 @@ 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/releases/yarn-1.22.17.cjs",
yarn = YARN_LABEL,
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()
@@ -158,10 +214,10 @@ cldr_xml_data_repository(
# sass rules
http_archive(
name = "io_bazel_rules_sass",
sha256 = "0cde2dd9ff34994f27ba4d5c74d34cca3c727bf41bc6024cf66ad298a1f5e5c2",
strip_prefix = "rules_sass-f6ceac7f5e11424880ae41f9c1a5cfd02968376c",
sha256 = "618f0b7aae019c149ac2ff4142a66c110de116fb5c173dbeccedbaee06fc5f6d",
strip_prefix = "rules_sass-a2fce75bcf103750f7accbf7f966ad48bcbca2d4",
urls = [
"https://github.com/bazelbuild/rules_sass/archive/f6ceac7f5e11424880ae41f9c1a5cfd02968376c.zip",
"https://github.com/bazelbuild/rules_sass/archive/a2fce75bcf103750f7accbf7f966ad48bcbca2d4.zip",
],
)
@@ -169,5 +225,13 @@ http_archive(
load("@io_bazel_rules_sass//sass:sass_repositories.bzl", "sass_repositories")
sass_repositories(
yarn_script = "//:.yarn/releases/yarn-1.22.17.cjs",
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",
)
-16
View File
@@ -1,16 +0,0 @@
# 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
+4 -1
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,6 +43,9 @@ testem.log
# e2e
protractor-results*.txt
# Example playground
content/example-playground
# System files
.DS_Store
Thumbs.db
+308 -34
View File
@@ -1,39 +1,137 @@
load("@aio_npm//@angular-devkit/architect-cli:index.bzl", "architect", "architect_test")
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")
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")
exports_files([
"firebase.json",
"ngsw-config.json",
"ngsw-config.template.json",
"tsconfig.json",
])
# Generate ngsw-config
npm_package_bin(
name = "ngsw-config",
outs = ["ngsw-config_generated.json"],
args = ["$@"],
tool = "//aio/scripts:build-ngsw-config",
# 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,
)
# 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",
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
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:always",
# 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__"],
)
# 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",
},
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 = [
@@ -45,14 +143,20 @@ APPLICATION_FILES = [
],
)
# External dependencies from aio/package.json required to build the docs app.
# Dependencies 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",
@@ -78,8 +182,9 @@ TEST_FILES = APPLICATION_FILES + [
["src/**/*.spec.ts"],
)
# External dependencies from aio/package.json required to test the docs app
# Dependencies 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",
@@ -89,28 +194,197 @@ TEST_DEPS = APPLICATION_DEPS + [
"@aio_npm//karma-coverage",
"@aio_npm//karma-jasmine",
"@aio_npm//karma-jasmine-html-reporter",
"@aio_npm//puppeteer",
"//aio/tools:windows-chromium-path",
]
architect(
name = "build",
args = [
"site:build:stable",
"--outputPath=../$(@D)",
],
chdir = package_name(),
configuration_env_vars = ["NG_BUILD_CACHE"],
data = APPLICATION_FILES + APPLICATION_DEPS,
output_dir = True,
# 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_test(
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",
],
replace_prefixes = {
"build-app": "",
},
visibility = ["//aio/scripts/deploy-to-firebase:__pkg__"],
)
TEST_DATA = TEST_FILES + select({
":aio_local_deps": substitute_local_package_deps(TEST_DEPS),
"//conditions:default": TEST_DEPS,
})
aio_test(
name = "test",
args = [
"site:test",
"--no-watch",
],
chdir = package_name(),
configuration_env_vars = ["NG_BUILD_CACHE"],
data = TEST_FILES + TEST_DEPS,
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)",
},
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",
)
+24 -47
View File
@@ -9,56 +9,46 @@ 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 to run build 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.
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 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 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 lint` - check that the doc-viewer code follows our style rules.
* `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-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-lint` - check that the doc gen code follows our style rules.
* `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 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-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 example-e2e` - run all e2e tests for examples. Available options:
- `--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._
- `--local`: run e2e tests against locally built Angular packages.
- `--filter=foo`: limit e2e tests to those containing the word "foo".
- `--exclude=bar`: exclude e2e tests containing the word "bar".
> **Note for Windows users**
>
> 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`
> 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.
## 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 in `dist/` with `yarn http-server dist -p 4200`.
serve the files with `yarn http-server ../dist/bin/aio/build -p 4200`.
## Guide to authoring
@@ -69,7 +59,7 @@ There are two types of content in the documentation:
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.
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.
* **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.
@@ -87,7 +77,7 @@ writing guide pages, explains how to use the documentation classes and component
### Generating the complete docs
The main task for generating the docs is `yarn docs`. This will process all the source files (API and other),
Running the `yarn build` or `yarn start` tasks will automatically generate the 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
@@ -109,26 +99,13 @@ correctly.
The general setup is as follows:
* 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
* Open a terminal, ensure the dependencies are installed, then start the doc-viewer:
```bash
yarn docs-watch
```
>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.
* A browser will open automatically at https://localhost:4200/. Navigate to the document on which you want to work.
* 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.
@@ -1,3 +0,0 @@
scripts-js/lib
scripts-js/node_modules
scripts-js/**/test
-179
View File
@@ -1,179 +0,0 @@
# 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=36700160
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
@@ -1,2 +0,0 @@
# 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
@@ -1,16 +0,0 @@
# 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
@@ -1,9 +0,0 @@
/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
}
@@ -1,13 +0,0 @@
/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
}
@@ -1,9 +0,0 @@
/var/log/aio/preview-server-*.log {
compress
copytruncate
delaycompress
missingok
monthly
notifempty
rotate 6
}
@@ -1,123 +0,0 @@
# 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;
}
}
@@ -1,20 +0,0 @@
#!/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
@@ -1,2 +0,0 @@
/dist
/node_modules
@@ -1,128 +0,0 @@
// 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)));
}
}
@@ -1,26 +0,0 @@
// 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));
}
@@ -1,151 +0,0 @@
// 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})`);
}
}
}
@@ -1,4 +0,0 @@
// Constants
export const AIO_DOWNLOADS_DIR = '/tmp/aio-downloads';
export const HIDDEN_DIR_PREFIX = 'hidden--';
export const SHORT_SHA_LEN = 7;
@@ -1,19 +0,0 @@
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');
@@ -1,111 +0,0 @@
// 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('&');
}
}
@@ -1,79 +0,0 @@
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`);
}
}
@@ -1,72 +0,0 @@
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;
}
}
}
@@ -1,22 +0,0 @@
// 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();
};
@@ -1,109 +0,0 @@
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);
}
}
@@ -1,144 +0,0 @@
// 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));
}
}
@@ -1,16 +0,0 @@
// 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) {}
}
@@ -1,77 +0,0 @@
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})`);
}
}
}
@@ -1,46 +0,0 @@
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);
}
}
@@ -1,41 +0,0 @@
// 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);
}
@@ -1,8 +0,0 @@
// Classes
export class PreviewServerError extends Error {
// Constructor
constructor(public status: number = 500, message?: string) {
super(message);
Object.setPrototypeOf(this, PreviewServerError.prototype);
}
}
@@ -1,207 +0,0 @@
// 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;
}
}
@@ -1,28 +0,0 @@
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);
}
@@ -1,49 +0,0 @@
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';
@@ -1,10 +0,0 @@
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[];
}
@@ -1,237 +0,0 @@
// 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();
@@ -1,6 +0,0 @@
// Imports
import {runTests} from '../common/run-tests';
// Run
const specFiles = [`${__dirname}/**/*.e2e.js`];
runTests(specFiles);
@@ -1,7 +0,0 @@
declare module jasmine {
interface Matchers {
toExistAsAFile(remove = true): boolean;
toExistAsABuild(remove = true): boolean;
toExistAsAnArtifact(remove = true): boolean;
}
}
@@ -1,88 +0,0 @@
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(),
};
@@ -1,187 +0,0 @@
/* 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);
@@ -1,395 +0,0 @@
// 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);
});
});
});
}));
});
@@ -1,566 +0,0 @@
// 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)),
]);
});
});
});
@@ -1,269 +0,0 @@
// 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();
});
});
}));
@@ -1,2 +0,0 @@
import '../preview-server';
import './mock-external-apis';
@@ -1,30 +0,0 @@
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;
}
@@ -1,54 +0,0 @@
{
"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.8.0"
}
}
@@ -1,433 +0,0 @@
// Imports
import * as fs from 'fs';
import {normalize} from 'path';
import * as shell from 'shelljs';
import {BuildCleaner} from '../../lib/clean-up/build-cleaner';
import {HIDDEN_DIR_PREFIX} from '../../lib/common/constants';
import {GithubPullRequests} from '../../lib/common/github-pull-requests';
import {Logger} from '../../lib/common/utils';
const EXISTING_BUILDS = [10, 20, 30, 40];
const EXISTING_DOWNLOADS = [
'10-ABCDEF0-build.zip',
'10-1234567-build.zip',
'20-ABCDEF0-build.zip',
'20-1234567-build.zip',
];
const OPEN_PRS = [10, 40];
// Tests
describe('BuildCleaner', () => {
let loggerErrorSpy: jasmine.Spy;
let loggerLogSpy: jasmine.Spy;
let cleaner: BuildCleaner;
beforeEach(() => {
loggerErrorSpy = spyOn(Logger.prototype, 'error');
loggerLogSpy = spyOn(Logger.prototype, 'log');
cleaner = new BuildCleaner('/foo/bar', 'baz', 'qux', '12345', '/downloads', 'build.zip');
});
describe('constructor()', () => {
it('should throw if \'buildsDir\' is empty', () => {
expect(() => new BuildCleaner('', 'baz', 'qux', '12345', 'downloads', 'build.zip')).
toThrowError('Missing or empty required parameter \'buildsDir\'!');
});
it('should throw if \'githubOrg\' is empty', () => {
expect(() => new BuildCleaner('/foo/bar', '', 'qux', '12345', 'downloads', 'build.zip')).
toThrowError('Missing or empty required parameter \'githubOrg\'!');
});
it('should throw if \'githubRepo\' is empty', () => {
expect(() => new BuildCleaner('/foo/bar', 'baz', '', '12345', 'downloads', 'build.zip')).
toThrowError('Missing or empty required parameter \'githubRepo\'!');
});
it('should throw if \'githubToken\' is empty', () => {
expect(() => new BuildCleaner('/foo/bar', 'baz', 'qux', '', 'downloads', 'build.zip')).
toThrowError('Missing or empty required parameter \'githubToken\'!');
});
it('should throw if \'downloadsDir\' is empty', () => {
expect(() => new BuildCleaner('/foo/bar', 'baz', 'qux', '12345', '', 'build.zip')).
toThrowError('Missing or empty required parameter \'downloadsDir\'!');
});
it('should throw if \'artifactPath\' is empty', () => {
expect(() => new BuildCleaner('/foo/bar', 'baz', 'qux', '12345', 'downloads', '')).
toThrowError('Missing or empty required parameter \'artifactPath\'!');
});
});
describe('cleanUp()', () => {
let cleanerGetExistingBuildNumbersSpy: jasmine.Spy;
let cleanerGetOpenPrNumbersSpy: jasmine.Spy;
let cleanerGetExistingDownloadsSpy: jasmine.Spy;
let cleanerRemoveUnnecessaryBuildsSpy: jasmine.Spy;
let cleanerRemoveUnnecessaryDownloadsSpy: jasmine.Spy;
beforeEach(() => {
cleanerGetExistingBuildNumbersSpy = spyOn(cleaner, 'getExistingBuildNumbers').and.resolveTo(EXISTING_BUILDS);
cleanerGetOpenPrNumbersSpy = spyOn(cleaner, 'getOpenPrNumbers').and.resolveTo(OPEN_PRS);
cleanerGetExistingDownloadsSpy = spyOn(cleaner, 'getExistingDownloads').and.resolveTo(EXISTING_DOWNLOADS);
cleanerRemoveUnnecessaryBuildsSpy = spyOn(cleaner, 'removeUnnecessaryBuilds');
cleanerRemoveUnnecessaryDownloadsSpy = spyOn(cleaner, 'removeUnnecessaryDownloads');
});
it('should return a promise', async () => {
const promise = cleaner.cleanUp();
expect(promise).toBeInstanceOf(Promise);
// Do not complete the test and release the spies synchronously, to avoid running the actual implementations.
await promise;
});
it('should get the open PRs', async () => {
await cleaner.cleanUp();
expect(cleanerGetOpenPrNumbersSpy).toHaveBeenCalled();
});
it('should get the existing builds', async () => {
await cleaner.cleanUp();
expect(cleanerGetExistingBuildNumbersSpy).toHaveBeenCalled();
});
it('should get the existing downloads', async () => {
await cleaner.cleanUp();
expect(cleanerGetExistingDownloadsSpy).toHaveBeenCalled();
});
it('should pass existing builds and open PRs to \'removeUnnecessaryBuilds()\'', async () => {
await cleaner.cleanUp();
expect(cleanerRemoveUnnecessaryBuildsSpy).toHaveBeenCalledWith(EXISTING_BUILDS, OPEN_PRS);
});
it('should pass existing downloads and open PRs to \'removeUnnecessaryDownloads()\'', async () => {
await cleaner.cleanUp();
expect(cleanerRemoveUnnecessaryDownloadsSpy).toHaveBeenCalledWith(EXISTING_DOWNLOADS, OPEN_PRS);
});
it('should reject if \'getOpenPrNumbers()\' rejects', async () => {
cleanerGetOpenPrNumbersSpy.and.rejectWith('Test');
await expectAsync(cleaner.cleanUp()).toBeRejectedWith('Test');
});
it('should reject if \'getExistingBuildNumbers()\' rejects', async () => {
cleanerGetExistingBuildNumbersSpy.and.rejectWith('Test');
await expectAsync(cleaner.cleanUp()).toBeRejectedWith('Test');
});
it('should reject if \'getExistingDownloads()\' rejects', async () => {
cleanerGetExistingDownloadsSpy.and.rejectWith('Test');
await expectAsync(cleaner.cleanUp()).toBeRejectedWith('Test');
});
it('should reject if \'removeUnnecessaryBuilds()\' rejects', async () => {
cleanerRemoveUnnecessaryBuildsSpy.and.rejectWith('Test');
await expectAsync(cleaner.cleanUp()).toBeRejectedWith('Test');
});
it('should reject if \'removeUnnecessaryDownloads()\' rejects', async () => {
cleanerRemoveUnnecessaryDownloadsSpy.and.rejectWith('Test');
await expectAsync(cleaner.cleanUp()).toBeRejectedWith('Test');
});
});
describe('getExistingBuildNumbers()', () => {
let fsReaddirSpy: jasmine.Spy;
let readdirCb: (err: any, files?: string[]) => void;
let promise: Promise<number[]>;
beforeEach(() => {
fsReaddirSpy = spyOn(fs, 'readdir').and.callFake(
((_: string, cb: typeof readdirCb) => readdirCb = cb) as unknown as typeof fs.readdir,
);
promise = cleaner.getExistingBuildNumbers();
});
it('should return a promise', () => {
expect(promise).toBeInstanceOf(Promise);
});
it('should get the contents of the builds directory', () => {
expect(fsReaddirSpy).toHaveBeenCalled();
expect(fsReaddirSpy.calls.argsFor(0)[0]).toBe('/foo/bar');
});
it('should reject if an error occurs while getting the files', async () => {
readdirCb('Test');
await expectAsync(promise).toBeRejectedWith('Test');
});
it('should resolve with the returned files (as numbers)', async () => {
readdirCb(null, ['12', '34', '56']);
await expectAsync(promise).toBeResolvedTo([12, 34, 56]);
});
it('should remove `HIDDEN_DIR_PREFIX` from the filenames', async () => {
readdirCb(null, [`${HIDDEN_DIR_PREFIX}12`, '34', `${HIDDEN_DIR_PREFIX}56`]);
await expectAsync(promise).toBeResolvedTo([12, 34, 56]);
});
it('should ignore files with non-numeric (or zero) names', async () => {
readdirCb(null, ['12', 'foo', '34', 'bar', '56', '000']);
await expectAsync(promise).toBeResolvedTo([12, 34, 56]);
});
});
describe('getOpenPrNumbers()', () => {
let prDeferred: {resolve: (v: any) => void, reject: (v: any) => void};
let promise: Promise<number[]>;
beforeEach(() => {
spyOn(GithubPullRequests.prototype, 'fetchAll').and.callFake(() => {
return new Promise((resolve, reject) => prDeferred = {resolve, reject});
});
promise = cleaner.getOpenPrNumbers();
});
it('should return a promise', () => {
expect(promise).toBeInstanceOf(Promise);
});
it('should fetch open PRs via \'GithubPullRequests\'', () => {
expect(GithubPullRequests.prototype.fetchAll).toHaveBeenCalledWith('open');
});
it('should reject if an error occurs while fetching PRs', async () => {
prDeferred.reject('Test');
await expectAsync(promise).toBeRejectedWith('Test');
});
it('should resolve with the numbers of the fetched PRs', async () => {
prDeferred.resolve([{id: 0, number: 1}, {id: 1, number: 2}, {id: 2, number: 3}]);
await expectAsync(promise).toBeResolvedTo([1, 2, 3]);
});
});
describe('getExistingDownloads()', () => {
let fsReaddirSpy: jasmine.Spy;
let readdirCb: (err: any, files?: string[]) => void;
let promise: Promise<string[]>;
beforeEach(() => {
fsReaddirSpy = spyOn(fs, 'readdir').and.callFake(
((_: string, cb: typeof readdirCb) => readdirCb = cb) as unknown as typeof fs.readdir,
);
promise = cleaner.getExistingDownloads();
});
it('should return a promise', () => {
expect(promise).toBeInstanceOf(Promise);
});
it('should get the contents of the downloads directory', () => {
expect(fsReaddirSpy).toHaveBeenCalled();
expect(fsReaddirSpy.calls.argsFor(0)[0]).toBe('/downloads');
});
it('should reject if an error occurs while getting the files', async () => {
readdirCb('Test');
await expectAsync(promise).toBeRejectedWith('Test');
});
it('should resolve with the returned file names', async () => {
readdirCb(null, EXISTING_DOWNLOADS);
await expectAsync(promise).toBeResolvedTo(EXISTING_DOWNLOADS);
});
it('should ignore files that do not match the artifactPath', async () => {
readdirCb(null, ['10-ABCDEF-build.zip', '20-AAAAAAA-otherfile.zip', '30-FFFFFFF-build.zip']);
await expectAsync(promise).toBeResolvedTo(['10-ABCDEF-build.zip', '30-FFFFFFF-build.zip']);
});
});
describe('removeDir()', () => {
let shellChmodSpy: jasmine.Spy;
let shellRmSpy: jasmine.Spy;
let shellTestSpy: jasmine.Spy;
beforeEach(() => {
shellChmodSpy = spyOn(shell, 'chmod');
shellRmSpy = spyOn(shell, 'rm');
shellTestSpy = spyOn(shell, 'test').and.returnValue(true);
});
it('should test if the directory exists (and return if it does not)', () => {
shellTestSpy.and.returnValue(false);
cleaner.removeDir('/foo/bar');
expect(shellTestSpy).toHaveBeenCalledWith('-d', '/foo/bar');
expect(shellChmodSpy).not.toHaveBeenCalled();
expect(shellRmSpy).not.toHaveBeenCalled();
});
it('should remove the specified directory and its content', () => {
cleaner.removeDir('/foo/bar');
expect(shellRmSpy).toHaveBeenCalledWith('-rf', '/foo/bar');
});
it('should make the directory and its content writable before removing', () => {
cleaner.removeDir('/foo/bar');
expect(shellChmodSpy).toHaveBeenCalledBefore(shellRmSpy);
expect(shellChmodSpy).toHaveBeenCalledWith('-R', 'a+w', '/foo/bar');
expect(shellRmSpy).toHaveBeenCalled();
});
it('should catch errors and log them', () => {
shellRmSpy.and.throwError('Test');
cleaner.removeDir('/foo/bar');
expect(loggerErrorSpy).toHaveBeenCalledWith('ERROR: Unable to remove \'/foo/bar\' due to:', new Error('Test'));
});
});
describe('removeUnnecessaryBuilds()', () => {
let cleanerRemoveDirSpy: jasmine.Spy;
beforeEach(() => {
cleanerRemoveDirSpy = spyOn(cleaner, 'removeDir');
});
it('should log the number of existing builds and builds to be removed', () => {
cleaner.removeUnnecessaryBuilds([1, 2, 3], [3, 4, 5, 6]);
expect(loggerLogSpy).toHaveBeenCalledWith('Existing builds: 3');
expect(loggerLogSpy).toHaveBeenCalledWith('Removing 2 build(s): 1, 2');
});
it('should construct full paths to directories (by prepending \'buildsDir\')', () => {
cleaner.removeUnnecessaryBuilds([1, 2, 3], []);
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(normalize('/foo/bar/1'));
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(normalize('/foo/bar/2'));
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(normalize('/foo/bar/3'));
});
it('should try removing hidden directories as well', () => {
cleaner.removeUnnecessaryBuilds([1, 2, 3], []);
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(normalize(`/foo/bar/${HIDDEN_DIR_PREFIX}1`));
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(normalize(`/foo/bar/${HIDDEN_DIR_PREFIX}2`));
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(normalize(`/foo/bar/${HIDDEN_DIR_PREFIX}3`));
});
it('should remove the builds that do not correspond to open PRs', () => {
cleaner.removeUnnecessaryBuilds([1, 2, 3, 4], [2, 4]);
expect(cleanerRemoveDirSpy).toHaveBeenCalledTimes(4);
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(normalize('/foo/bar/1'));
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(normalize('/foo/bar/3'));
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(normalize(`/foo/bar/${HIDDEN_DIR_PREFIX}1`));
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(normalize(`/foo/bar/${HIDDEN_DIR_PREFIX}3`));
cleanerRemoveDirSpy.calls.reset();
cleaner.removeUnnecessaryBuilds([1, 2, 3, 4], [1, 2, 3, 4]);
expect(cleanerRemoveDirSpy).toHaveBeenCalledTimes(0);
cleanerRemoveDirSpy.calls.reset();
cleaner.removeUnnecessaryBuilds([1, 2, 3, 4], []);
expect(cleanerRemoveDirSpy).toHaveBeenCalledTimes(8);
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(normalize('/foo/bar/1'));
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(normalize('/foo/bar/2'));
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(normalize('/foo/bar/3'));
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(normalize('/foo/bar/4'));
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(normalize(`/foo/bar/${HIDDEN_DIR_PREFIX}1`));
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(normalize(`/foo/bar/${HIDDEN_DIR_PREFIX}2`));
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(normalize(`/foo/bar/${HIDDEN_DIR_PREFIX}3`));
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(normalize(`/foo/bar/${HIDDEN_DIR_PREFIX}4`));
cleanerRemoveDirSpy.calls.reset();
});
});
describe('removeUnnecessaryDownloads()', () => {
let shellRmSpy: jasmine.Spy;
beforeEach(() => {
shellRmSpy = spyOn(shell, 'rm');
});
it('should log the number of existing downloads and downloads to be removed', () => {
cleaner.removeUnnecessaryDownloads(EXISTING_DOWNLOADS, OPEN_PRS);
expect(loggerLogSpy).toHaveBeenCalledWith('Existing downloads: 4');
expect(loggerLogSpy).toHaveBeenCalledWith('Removing 2 download(s): 20-ABCDEF0-build.zip, 20-1234567-build.zip');
});
it('should construct full paths to directories (by prepending \'downloadsDir\')', () => {
cleaner.removeUnnecessaryDownloads(['dl-1', 'dl-2', 'dl-3'], []);
expect(shellRmSpy).toHaveBeenCalledWith(normalize('/downloads/dl-1'));
expect(shellRmSpy).toHaveBeenCalledWith(normalize('/downloads/dl-2'));
expect(shellRmSpy).toHaveBeenCalledWith(normalize('/downloads/dl-3'));
});
it('should remove the downloads that do not correspond to open PRs', () => {
cleaner.removeUnnecessaryDownloads(EXISTING_DOWNLOADS, OPEN_PRS);
expect(shellRmSpy).toHaveBeenCalledTimes(2);
expect(shellRmSpy).toHaveBeenCalledWith(normalize('/downloads/20-ABCDEF0-build.zip'));
expect(shellRmSpy).toHaveBeenCalledWith(normalize('/downloads/20-1234567-build.zip'));
});
});
});
@@ -1,154 +0,0 @@
import * as nock from 'nock';
import {CircleCiApi} from '../../lib/common/circle-ci-api';
const ORG = 'testorg';
const REPO = 'testrepo';
const TOKEN = 'xxxx';
const BASE_BUILD_URL = `https://circleci.com/api/v2/project/gh/${ORG}/${REPO}`;
const BASE_PIPELINE_URL = 'https://circleci.com/api/v2/pipeline';
describe('CircleCIApi', () => {
describe('constructor()', () => {
it('should throw if \'githubOrg\' is missing or empty', () => {
expect(() => new CircleCiApi('', REPO, TOKEN)).
toThrowError('Missing or empty required parameter \'githubOrg\'!');
});
it('should throw if \'githubRepo\' is missing or empty', () => {
expect(() => new CircleCiApi(ORG, '', TOKEN)).
toThrowError('Missing or empty required parameter \'githubRepo\'!');
});
it('should throw if \'circleCiToken\' is missing or empty', () => {
expect(() => new CircleCiApi(ORG, REPO, '')).
toThrowError('Missing or empty required parameter \'circleCiToken\'!');
});
});
describe('fetchFromCircleCI', () => {
it('should include the authentication token in the headers on every request', async () => {
const api = new CircleCiApi(ORG, REPO, TOKEN);
const request = nock(BASE_BUILD_URL)
.get('/')
.matchHeader('Circle-Token', TOKEN)
.reply(200);
await api.fetchFromCircleCi(`${BASE_BUILD_URL}/`);
request.done();
})
})
describe('getBuildInfo', () => {
it('should make a request to the CircleCI API for the given build number', async () => {
const api = new CircleCiApi(ORG, REPO, TOKEN);
const buildNum = 12345;
const expectedBuildInfo: any = { org: ORG, repo: REPO, build_num: buildNum };
const request = nock(BASE_BUILD_URL)
.get(`/job/${buildNum}`)
.reply(200, expectedBuildInfo);
const buildInfo = await api.getBuildInfo(buildNum);
expect(buildInfo).toEqual(expectedBuildInfo);
request.done();
});
it('should throw an error if the request fails', async () => {
const api = new CircleCiApi(ORG, REPO, TOKEN);
const buildNum = 12345;
const errorMessage = 'Invalid request';
const request = nock(BASE_BUILD_URL).get(`/job/${buildNum}`);
request.replyWithError(errorMessage);
await expectAsync(api.getBuildInfo(buildNum)).toBeRejectedWithError(
`CircleCI build info request failed ` +
`(request to ${BASE_BUILD_URL}/job/${buildNum} failed, reason: ${errorMessage})`);
request.reply(404, errorMessage);
await expectAsync(api.getBuildInfo(buildNum)).toBeRejectedWithError(
`CircleCI build info request failed ` +
`(request to ${BASE_BUILD_URL}/job/${buildNum} failed, reason: ${errorMessage})`);
});
});
describe('getBuildArtifactUrl', () => {
it('should make a request to the CircleCI API for the given build number', async () => {
const api = new CircleCiApi(ORG, REPO, TOKEN);
const buildNum = 12345;
const artifact0: any = { path: 'some/path/0', url: 'https://url/0' };
const artifact1: any = { path: 'some/path/1', url: 'https://url/1' };
const artifact2: any = { path: 'some/path/2', url: 'https://url/2' };
const request = nock(BASE_BUILD_URL)
.get(`/${buildNum}/artifacts`)
.reply(200, {items: [artifact0, artifact1, artifact2]});
await expectAsync(api.getBuildArtifactUrl(buildNum, 'some/path/1')).toBeResolvedTo('https://url/1');
request.done();
});
it('should throw an error if the request fails', async () => {
const api = new CircleCiApi(ORG, REPO, TOKEN);
const buildNum = 12345;
const errorMessage = 'Invalid request';
const request = nock(BASE_BUILD_URL).get(`/${buildNum}/artifacts`);
request.replyWithError(errorMessage);
await expectAsync(api.getBuildArtifactUrl(buildNum, 'some/path/1')).toBeRejectedWithError(
`CircleCI artifact URL request failed ` +
`(request to ${BASE_BUILD_URL}/${buildNum}/artifacts failed, reason: ${errorMessage})`);
request.reply(404, errorMessage);
await expectAsync(api.getBuildArtifactUrl(buildNum, 'some/path/1')).toBeRejectedWithError(
`CircleCI artifact URL request failed ` +
`(request to ${BASE_BUILD_URL}/${buildNum}/artifacts failed, reason: ${errorMessage})`);
});
it('should throw an error if the response does not contain the specified artifact', async () => {
const api = new CircleCiApi(ORG, REPO, TOKEN);
const buildNum = 12345;
const artifact0: any = { path: 'some/path/0', url: 'https://url/0' };
const artifact1: any = { path: 'some/path/1', url: 'https://url/1' };
const artifact2: any = { path: 'some/path/2', url: 'https://url/2' };
nock(BASE_BUILD_URL)
.get(`/${buildNum}/artifacts`)
.reply(200, {items: [artifact0, artifact1, artifact2]});
await expectAsync(api.getBuildArtifactUrl(buildNum, 'some/path/3')).toBeRejectedWithError(
`CircleCI artifact URL request failed ` +
`(Missing artifact (some/path/3) for CircleCI build: ${buildNum})`);
});
});
describe('getPipelineInfo', () => {
it('should make a request to the CircleCI API for the given pipeline ID', async () => {
const api = new CircleCiApi(ORG, REPO, TOKEN);
const pipelineId = 'a1-b2-c3';
const expectedPipelineInfo: any = { org: ORG, repo: REPO, pipeline_id: pipelineId };
const request = nock(BASE_PIPELINE_URL)
.get(`/${pipelineId}`)
.reply(200, expectedPipelineInfo);
const pipelineInfo = await api.getPipelineInfo(pipelineId);
expect(pipelineInfo).toEqual(expectedPipelineInfo);
request.done();
});
it('should throw an error if the request fails', async () => {
const api = new CircleCiApi(ORG, REPO, TOKEN);
const pipelineId = 'a1-b2-c3';
const errorMessage = 'Invalid request';
const request = nock(BASE_PIPELINE_URL).get(`/${pipelineId}`);
request.replyWithError(errorMessage);
await expectAsync(api.getPipelineInfo(pipelineId)).toBeRejectedWithError(
`CircleCI pipeline info request failed ` +
`(request to ${BASE_PIPELINE_URL}/${pipelineId} failed, reason: ${errorMessage})`);
request.reply(404, errorMessage);
await expectAsync(api.getPipelineInfo(pipelineId)).toBeRejectedWithError(
`CircleCI pipeline info request failed ` +
`(request to ${BASE_PIPELINE_URL}/${pipelineId} failed, reason: ${errorMessage})`);
});
});
});
@@ -1,327 +0,0 @@
// Imports
import * as nock from 'nock';
import {GithubApi} from '../../lib/common/github-api';
// Tests
describe('GithubApi', () => {
let api: GithubApi;
beforeEach(() => api = new GithubApi('12345'));
describe('constructor()', () => {
it('should throw if \'githubToken\' is missing or empty', () => {
expect(() => new GithubApi('')).toThrowError('Missing or empty required parameter \'githubToken\'!');
});
});
describe('get()', () => {
let apiBuildPathSpy: jasmine.Spy;
let apiRequestSpy: jasmine.Spy;
beforeEach(() => {
apiBuildPathSpy = spyOn(api as any, 'buildPath');
apiRequestSpy = spyOn(api as any, 'request');
});
it('should call \'buildPath()\' with the pathname and params', () => {
api.get('/foo', {bar: 'baz'});
expect(apiBuildPathSpy).toHaveBeenCalled();
expect(apiBuildPathSpy.calls.argsFor(0)).toEqual(['/foo', {bar: 'baz'}]);
});
it('should call \'request()\' with the correct method', () => {
api.get('/foo');
expect(apiRequestSpy).toHaveBeenCalled();
expect(apiRequestSpy.calls.argsFor(0)[0]).toBe('get');
});
it('should call \'request()\' with the correct path', () => {
apiBuildPathSpy.and.returnValue('/foo/bar');
api.get('foo');
expect(apiRequestSpy).toHaveBeenCalled();
expect(apiRequestSpy.calls.argsFor(0)[1]).toBe('/foo/bar');
});
it('should not pass data to \'request()\'', () => {
(api.get as any)('foo', {}, {});
expect(apiRequestSpy).toHaveBeenCalled();
expect(apiRequestSpy.calls.argsFor(0)[2]).toBeUndefined();
});
});
describe('post()', () => {
let apiBuildPathSpy: jasmine.Spy;
let apiRequestSpy: jasmine.Spy;
beforeEach(() => {
apiBuildPathSpy = spyOn(api as any, 'buildPath');
apiRequestSpy = spyOn(api as any, 'request');
});
it('should call \'buildPath()\' with the pathname and params', () => {
api.post('/foo', {bar: 'baz'});
expect(apiBuildPathSpy).toHaveBeenCalled();
expect(apiBuildPathSpy.calls.argsFor(0)).toEqual(['/foo', {bar: 'baz'}]);
});
it('should call \'request()\' with the correct method', () => {
api.post('/foo');
expect(apiRequestSpy).toHaveBeenCalled();
expect(apiRequestSpy.calls.argsFor(0)[0]).toBe('post');
});
it('should call \'request()\' with the correct path', () => {
apiBuildPathSpy.and.returnValue('/foo/bar');
api.post('/foo');
expect(apiRequestSpy).toHaveBeenCalled();
expect(apiRequestSpy.calls.argsFor(0)[1]).toBe('/foo/bar');
});
it('should pass the data to \'request()\'', () => {
api.post('/foo', {}, {bar: 'baz'});
expect(apiRequestSpy).toHaveBeenCalled();
expect(apiRequestSpy.calls.argsFor(0)[2]).toEqual({bar: 'baz'});
});
});
describe('getPaginated()', () => {
let deferreds: {resolve: (v: any) => void, reject: (v: any) => void}[];
beforeEach(() => {
deferreds = [];
spyOn(api, 'get').and.callFake(() => new Promise((resolve, reject) => deferreds.push({resolve, reject})));
});
it('should return a promise', () => {
expect((api as any).getPaginated()).toBeInstanceOf(Promise);
});
it('should call \'get()\' with the correct pathname and params', () => {
(api as any).getPaginated('/foo/bar');
(api as any).getPaginated('/foo/bar', {baz: 'qux'});
expect(api.get).toHaveBeenCalledWith('/foo/bar', {page: 1, per_page: 100});
expect(api.get).toHaveBeenCalledWith('/foo/bar', {baz: 'qux', page: 1, per_page: 100});
});
it('should reject if the request fails', async () => {
const responsePromise = (api as any).getPaginated('/foo/bar');
deferreds[0].reject('Test');
await expectAsync(responsePromise).toBeRejectedWith('Test');
});
it('should resolve with the returned items', async () => {
const items = [{id: 1}, {id: 2}];
const responsePromise = (api as any).getPaginated('/foo/bar');
deferreds[0].resolve(items);
await expectAsync(responsePromise).toBeResolvedTo(items);
});
it('should iteratively call \'get()\' to fetch all items', async () => {
// Create an array or 250 objects.
const allItems = new Array(250).fill(null).map((_, i) => ({id: i}));
const apiGetSpy = api.get as jasmine.Spy;
const paramsForPage = (page: number) => ({baz: 'qux', page, per_page: 100});
const responsePromise = (api as any).getPaginated('/foo/bar', {baz: 'qux'});
deferreds[0].resolve(allItems.slice(0, 100));
setTimeout(() => {
deferreds[1].resolve(allItems.slice(100, 200));
setTimeout(() => {
deferreds[2].resolve(allItems.slice(200));
}, 0);
}, 0);
await expectAsync(responsePromise).toBeResolvedTo(allItems);
expect(apiGetSpy).toHaveBeenCalledTimes(3);
expect(apiGetSpy.calls.argsFor(0)).toEqual(['/foo/bar', paramsForPage(1)]);
expect(apiGetSpy.calls.argsFor(1)).toEqual(['/foo/bar', paramsForPage(2)]);
expect(apiGetSpy.calls.argsFor(2)).toEqual(['/foo/bar', paramsForPage(3)]);
});
});
// Protected methods
describe('buildPath()', () => {
it('should return the pathname if no params', () => {
expect((api as any).buildPath('/foo')).toBe('/foo');
expect((api as any).buildPath('/foo', undefined)).toBe('/foo');
expect((api as any).buildPath('/foo', null)).toBe('/foo');
});
it('should append the params to the pathname', () => {
expect((api as any).buildPath('/foo', {bar: 'baz'})).toBe('/foo?bar=baz');
});
it('should join the params with \'&\'', () => {
expect((api as any).buildPath('/foo', {bar: 1, baz: 2})).toBe('/foo?bar=1&baz=2');
});
it('should ignore undefined/null params', () => {
expect((api as any).buildPath('/foo', {bar: undefined, baz: null})).toBe('/foo');
});
it('should encode param values as URI components', () => {
expect((api as any).buildPath('/foo', {bar: 'b a&z'})).toBe('/foo?bar=b%20a%26z');
});
});
describe('request()', () => {
it('should return a promise', () => {
nock('https://api.github.com').get('/').reply(200);
expect((api as any).request()).toBeInstanceOf(Promise);
});
it('should call \'https.request()\' with the correct options', async () => {
const requestHandler = nock('https://api.github.com')
.intercept('/path', 'method')
.reply(200);
await (api as any).request('method', '/path');
requestHandler.done();
});
it('should add the \'Authorization\' header containing the \'githubToken\'', async () => {
const requestHandler = nock('https://api.github.com')
.intercept('/path', 'method', undefined, {
reqheaders: {Authorization: 'token 12345'},
})
.reply(200);
await (api as any).request('method', '/path');
requestHandler.done();
});
it('should reject on request error', async () => {
nock('https://api.github.com')
.intercept('/path', 'method')
.replyWithError('Test');
await expectAsync((api as any).request('method', '/path')).toBeRejectedWithError('Test');
});
it('should \'JSON.stringify\' and send the data along with the request', async () => {
const data = {key: 'value'};
const requestHandler = nock('https://api.github.com')
.intercept('/path', 'method', JSON.stringify(data))
.reply(200);
await (api as any).request('method', '/path', data);
requestHandler.done();
});
it('should reject if response statusCode is <200', async () => {
const requestHandler = nock('https://api.github.com')
.intercept('/path', 'method')
.reply(199);
const responsePromise = (api as any).request('method', '/path');
await expectAsync(responsePromise).toBeRejectedWith(jasmine.stringMatching('failed'));
await expectAsync(responsePromise).toBeRejectedWith(jasmine.stringMatching('status: 199'));
requestHandler.done();
});
it('should reject if response statusCode is >=400', async () => {
const requestHandler = nock('https://api.github.com')
.intercept('/path', 'method')
.reply(400);
const responsePromise = (api as any).request('method', '/path');
await expectAsync(responsePromise).toBeRejectedWith(jasmine.stringMatching('failed'));
await expectAsync(responsePromise).toBeRejectedWith(jasmine.stringMatching('status: 400'));
requestHandler.done();
});
it('should include the response text in the rejection message', async () => {
const requestHandler = nock('https://api.github.com')
.intercept('/path', 'method')
.reply(500, 'Test');
const responsePromise = (api as any).request('method', '/path');
await expectAsync(responsePromise).toBeRejectedWith(jasmine.stringMatching('Test'));
requestHandler.done();
});
it('should resolve if returned statusCode is >=200 and <400', async () => {
const requestHandler = nock('https://api.github.com')
.intercept('/path', 'method')
.reply(200);
await expectAsync((api as any).request('method', '/path')).toBeResolved();
requestHandler.done();
});
it('should parse the response body into an object using \'JSON.parse\'', async () => {
const requestHandler = nock('https://api.github.com')
.intercept('/path', 'method')
.reply(300, '{"foo": "bar"}');
await expectAsync((api as any).request('method', '/path')).toBeResolvedTo({foo: 'bar'});
requestHandler.done();
});
it('should reject if the response text is malformed JSON', async () => {
const requestHandler = nock('https://api.github.com')
.intercept('/path', 'method')
.reply(300, '}');
await expectAsync((api as any).request('method', '/path')).toBeRejectedWithError(SyntaxError);
requestHandler.done();
});
});
});
@@ -1,156 +0,0 @@
// Imports
import {GithubApi} from '../../lib/common/github-api';
import {GithubPullRequests, PullRequest} from '../../lib/common/github-pull-requests';
// Tests
describe('GithubPullRequests', () => {
let githubApi: jasmine.SpyObj<GithubApi>;
beforeEach(() => {
githubApi = jasmine.createSpyObj('githubApi', ['post', 'get', 'getPaginated']);
});
describe('constructor()', () => {
it('should throw if \'githubOrg\' is missing or empty', () => {
expect(() => new GithubPullRequests(githubApi, '', 'bar')).
toThrowError('Missing or empty required parameter \'githubOrg\'!');
});
it('should throw if \'githubRepo\' is missing or empty', () => {
expect(() => new GithubPullRequests(githubApi, 'foo', '')).
toThrowError('Missing or empty required parameter \'githubRepo\'!');
});
});
describe('addComment()', () => {
let prs: GithubPullRequests;
beforeEach(() => {
prs = new GithubPullRequests(githubApi, 'foo', 'bar');
});
it('should throw if the PR number is invalid', () => {
expect(() => prs.addComment(-1337, 'body')).toThrowError(`Invalid PR number: -1337`);
expect(() => prs.addComment(NaN, 'body')).toThrowError(`Invalid PR number: NaN`);
});
it('should throw if the comment body is invalid or empty', () => {
expect(() => prs.addComment(42, '')).toThrowError(`Invalid or empty comment body: `);
});
it('should make a POST request to Github with the correct pathname, params and data', () => {
githubApi.post.and.resolveTo();
prs.addComment(42, 'body');
expect(githubApi.post).toHaveBeenCalledWith('/repos/foo/bar/issues/42/comments', null, {body: 'body'});
});
it('should reject if the request fails', async () => {
githubApi.post.and.rejectWith('Test');
await expectAsync(prs.addComment(42, 'body')).toBeRejectedWith('Test');
});
it('should resolve with the data from the Github POST', async () => {
githubApi.post.and.resolveTo('Test');
await expectAsync(prs.addComment(42, 'body')).toBeResolvedTo('Test');
});
});
describe('fetch()', () => {
let prs: GithubPullRequests;
beforeEach(() => {
prs = new GithubPullRequests(githubApi, 'foo', 'bar');
});
it('should make a GET request to GitHub with the correct pathname', () => {
prs.fetch(42);
expect(githubApi.get).toHaveBeenCalledWith('/repos/foo/bar/issues/42');
});
it('should resolve with the data returned from GitHub', async () => {
const expected: any = {number: 42};
githubApi.get.and.resolveTo(expected);
await expectAsync(prs.fetch(42)).toBeResolvedTo(expected);
});
});
describe('fetchAll()', () => {
let prs: GithubPullRequests;
beforeEach(() => prs = new GithubPullRequests(githubApi, 'foo', 'bar'));
it('should call \'getPaginated()\' with the correct pathname and params', () => {
const expectedPathname = '/repos/foo/bar/pulls';
prs.fetchAll('all');
prs.fetchAll('closed');
prs.fetchAll('open');
expect(githubApi.getPaginated).toHaveBeenCalledTimes(3);
expect(githubApi.getPaginated.calls.argsFor(0)).toEqual([expectedPathname, {state: 'all'}]);
expect(githubApi.getPaginated.calls.argsFor(1)).toEqual([expectedPathname, {state: 'closed'}]);
expect(githubApi.getPaginated.calls.argsFor(2)).toEqual([expectedPathname, {state: 'open'}]);
});
it('should default to \'all\' if no state is specified', () => {
prs.fetchAll();
expect(githubApi.getPaginated).toHaveBeenCalledWith('/repos/foo/bar/pulls', {state: 'all'});
});
it('should forward the value returned by \'getPaginated()\'', async () => {
const mockPrs: PullRequest[] = [
{number: 1, user: {login: 'foo'}, labels: []},
{number: 2, user: {login: 'bar'}, labels: []},
];
githubApi.getPaginated.and.resolveTo(mockPrs);
expect(await prs.fetchAll()).toBe(mockPrs);
});
});
describe('fetchFiles()', () => {
let prs: GithubPullRequests;
beforeEach(() => {
prs = new GithubPullRequests(githubApi, 'foo', 'bar');
});
it('should make a paginated GET request to GitHub with the correct pathname', () => {
prs.fetchFiles(42);
expect(githubApi.getPaginated).toHaveBeenCalledWith('/repos/foo/bar/pulls/42/files');
});
it('should resolve with the data returned from GitHub', async () => {
const expected: any = [{sha: 'ABCDE', filename: 'a/b/c'}, {sha: '12345', filename: 'x/y/z'}];
githubApi.getPaginated.and.resolveTo(expected);
await expectAsync(prs.fetchFiles(42)).toBeResolvedTo(expected);
});
});
});
@@ -1,203 +0,0 @@
import {GithubApi} from '../../lib/common/github-api';
import {GithubTeams, Team} from '../../lib/common/github-teams';
// Tests
describe('GithubTeams', () => {
let githubApi: jasmine.SpyObj<GithubApi>;
beforeEach(() => {
githubApi = jasmine.createSpyObj('githubApi', ['post', 'get', 'getPaginated']);
});
describe('constructor()', () => {
it('should throw if \'githubOrg\' is missing or empty', () => {
expect(() => new GithubTeams(githubApi, '')).
toThrowError('Missing or empty required parameter \'githubOrg\'!');
});
});
describe('fetchAll()', () => {
let teams: GithubTeams;
beforeEach(() => {
teams = new GithubTeams(githubApi, 'foo');
});
it('should call \'getPaginated()\' with the correct pathname and params', () => {
teams.fetchAll();
expect(githubApi.getPaginated).toHaveBeenCalledWith('/orgs/foo/teams');
});
it('should forward the value returned by \'getPaginated()\'', async () => {
const mockTeams: Team[] = [
{id: 1, slug: 'foo'},
{id: 2, slug: 'bar'},
];
githubApi.getPaginated.and.resolveTo(mockTeams);
expect(await teams.fetchAll()).toBe(mockTeams);
});
});
describe('isMemberById()', () => {
let teams: GithubTeams;
beforeEach(() => {
teams = new GithubTeams(githubApi, 'foo');
});
it('should return a promise', () => {
githubApi.get.and.resolveTo();
const promise = teams.isMemberById('user', [1]);
expect(promise).toBeInstanceOf(Promise);
});
it('should resolve with false if called with an empty array', async () => {
await expectAsync(teams.isMemberById('user', [])).toBeResolvedTo(false);
expect(githubApi.get).not.toHaveBeenCalled();
});
it('should call \'get()\' with the correct pathname', async () => {
githubApi.get.and.resolveTo();
await teams.isMemberById('user', [1]);
expect(githubApi.get).toHaveBeenCalledWith('/teams/1/memberships/user');
});
it('should resolve with false if \'get()\' rejects', async () => {
githubApi.get.and.rejectWith(null);
await expectAsync(teams.isMemberById('user', [1])).toBeResolvedTo(false);
expect(githubApi.get).toHaveBeenCalled();
});
it('should resolve with false if the membership is not active', async () => {
githubApi.get.and.resolveTo({state: 'pending'});
await expectAsync(teams.isMemberById('user', [1])).toBeResolvedTo(false);
expect(githubApi.get).toHaveBeenCalled();
});
it('should resolve with true if the membership is active', async () => {
githubApi.get.and.resolveTo({state: 'active'});
await expectAsync(teams.isMemberById('user', [1])).toBeResolvedTo(true);
});
it('should sequentially call \'get()\' until an active membership is found', async () => {
githubApi.get.
withArgs('/teams/1/memberships/user').and.resolveTo({state: 'pending'}).
withArgs('/teams/2/memberships/user').and.rejectWith(null).
withArgs('/teams/3/memberships/user').and.resolveTo({state: 'active'});
await expectAsync(teams.isMemberById('user', [1, 2, 3, 4])).toBeResolvedTo(true);
expect(githubApi.get).toHaveBeenCalledTimes(3);
expect(githubApi.get.calls.argsFor(0)[0]).toBe('/teams/1/memberships/user');
expect(githubApi.get.calls.argsFor(1)[0]).toBe('/teams/2/memberships/user');
expect(githubApi.get.calls.argsFor(2)[0]).toBe('/teams/3/memberships/user');
});
it('should resolve with false if no active membership is found', async () => {
githubApi.get.
withArgs('/teams/1/memberships/user').and.resolveTo({state: 'pending'}).
withArgs('/teams/2/memberships/user').and.rejectWith(null).
withArgs('/teams/3/memberships/user').and.resolveTo({state: 'not active'}).
withArgs('/teams/4/memberships/user').and.rejectWith(null);
await expectAsync(teams.isMemberById('user', [1, 2, 3, 4])).toBeResolvedTo(false);
expect(githubApi.get).toHaveBeenCalledTimes(4);
expect(githubApi.get.calls.argsFor(0)[0]).toBe('/teams/1/memberships/user');
expect(githubApi.get.calls.argsFor(1)[0]).toBe('/teams/2/memberships/user');
expect(githubApi.get.calls.argsFor(2)[0]).toBe('/teams/3/memberships/user');
expect(githubApi.get.calls.argsFor(3)[0]).toBe('/teams/4/memberships/user');
});
});
describe('isMemberBySlug()', () => {
let teams: GithubTeams;
let teamsFetchAllSpy: jasmine.Spy;
let teamsIsMemberByIdSpy: jasmine.Spy;
beforeEach(() => {
teams = new GithubTeams(githubApi, 'foo');
teamsFetchAllSpy = spyOn(teams, 'fetchAll').and.resolveTo([{id: 1, slug: 'team1'}, {id: 2, slug: 'team2'}]);
teamsIsMemberByIdSpy = spyOn(teams, 'isMemberById');
});
it('should return a promise', () => {
expect(teams.isMemberBySlug('user', ['team-slug'])).toBeInstanceOf(Promise);
});
it('should call \'fetchAll()\'', () => {
teams.isMemberBySlug('user', ['team-slug']);
expect(teamsFetchAllSpy).toHaveBeenCalled();
});
it('should resolve with false if \'fetchAll()\' rejects', async () => {
teamsFetchAllSpy.and.rejectWith(null);
await expectAsync(teams.isMemberBySlug('user', ['team-slug'])).toBeResolvedTo(false);
});
it('should call \'isMemberById()\' with the correct params if no team is found', async () => {
await teams.isMemberBySlug('user', ['no-match']);
expect(teamsIsMemberByIdSpy).toHaveBeenCalledWith('user', []);
});
it('should call \'isMemberById()\' with the correct params if teams are found', async () => {
await teams.isMemberBySlug('userA', ['team1']);
expect(teamsIsMemberByIdSpy).toHaveBeenCalledWith('userA', [1]);
await teams.isMemberBySlug('userB', ['team2']);
expect(teamsIsMemberByIdSpy).toHaveBeenCalledWith('userB', [2]);
await teams.isMemberBySlug('userC', ['team1', 'team2']);
expect(teamsIsMemberByIdSpy).toHaveBeenCalledWith('userC', [1, 2]);
});
it('should resolve with false if \'isMemberById()\' rejects', async () => {
teamsIsMemberByIdSpy.and.rejectWith(null);
await expectAsync(teams.isMemberBySlug('user', ['team1'])).toBeResolvedTo(false);
expect(teamsIsMemberByIdSpy).toHaveBeenCalled();
});
it('should resolve with the value \'isMemberById()\' resolves with', async () => {
teamsIsMemberByIdSpy.and.resolveTo(true);
await expectAsync(teams.isMemberBySlug('userA', ['team1'])).toBeResolvedTo(true);
expect(teamsIsMemberByIdSpy).toHaveBeenCalledWith('userA', [1]);
teamsIsMemberByIdSpy.and.resolveTo(false);
await expectAsync(teams.isMemberBySlug('userB', ['team1'])).toBeResolvedTo(false);
expect(teamsIsMemberByIdSpy).toHaveBeenCalledWith('userB', [1]);
});
});
});
@@ -1,206 +0,0 @@
// Imports
import {resolve as resolvePath} from 'path';
import {
assert,
assertNotMissingOrEmpty,
computeArtifactDownloadPath,
computeShortSha,
getEnvVar,
getPrInfoFromDownloadPath,
Logger,
} from '../../lib/common/utils';
// Tests
describe('utils', () => {
describe('computeShortSha', () => {
it('should return only the first SHORT_SHA_LEN characters of the SHA', () => {
expect(computeShortSha('0123456789')).toEqual('0123456');
expect(computeShortSha('ABC')).toEqual('ABC');
expect(computeShortSha('')).toEqual('');
});
});
describe('assert', () => {
it('should throw if passed a false value', () => {
expect(() => assert(false, 'error message')).toThrowError('error message');
});
it('should not throw if passed a true value', () => {
expect(() => assert(true, 'error message')).not.toThrow();
});
});
describe('computeArtifactDownloadPath', () => {
it('should compute an absolute path based on the artifact info provided', () => {
const downloadDir = '/a/b/c';
const pr = 123;
const sha = 'ABCDEF1234567';
const artifactPath = 'a/path/to/file.zip';
const path = computeArtifactDownloadPath(downloadDir, pr, sha, artifactPath);
expect(path).toBe(resolvePath('/a/b/c/123-ABCDEF1-file.zip'));
});
});
describe('getPrInfoFromDownloadPath', () => {
it('should extract the PR and SHA from the file path', () => {
const {pr, sha} = getPrInfoFromDownloadPath('a/b/c/12345-ABCDE-artifact.zip');
expect(pr).toEqual(12345);
expect(sha).toEqual('ABCDE');
});
});
describe('assertNotMissingOrEmpty()', () => {
it('should throw if passed an empty value', () => {
expect(() => assertNotMissingOrEmpty('foo', undefined)).
toThrowError('Missing or empty required parameter \'foo\'!');
expect(() => assertNotMissingOrEmpty('bar', null)).toThrowError('Missing or empty required parameter \'bar\'!');
expect(() => assertNotMissingOrEmpty('baz', '')).toThrowError('Missing or empty required parameter \'baz\'!');
});
it('should not throw if passed a non-empty value', () => {
expect(() => assertNotMissingOrEmpty('foo', ' ')).not.toThrow();
expect(() => assertNotMissingOrEmpty('bar', 'bar')).not.toThrow();
expect(() => assertNotMissingOrEmpty('baz', 'b a z')).not.toThrow();
});
});
describe('getEnvVar()', () => {
const emptyVar = '$$test_utils_getEnvVar_empty$$';
const nonEmptyVar = '$$test_utils_getEnvVar_nonEmpty$$';
const undefinedVar = '$$test_utils_getEnvVar_undefined$$';
beforeEach(() => {
process.env[emptyVar] = '';
process.env[nonEmptyVar] = 'foo';
});
afterEach(() => {
delete process.env[emptyVar];
delete process.env[nonEmptyVar];
});
it('should return an environment variable', () => {
expect(getEnvVar(nonEmptyVar)).toBe('foo');
});
it('should exit with an error if the environment variable is not defined', () => {
const consoleErrorSpy = spyOn(console, 'error');
const processExitSpy = spyOn(process, 'exit');
getEnvVar(undefinedVar);
expect(consoleErrorSpy).toHaveBeenCalled();
expect(consoleErrorSpy.calls.argsFor(0)[0]).toContain(undefinedVar);
expect(processExitSpy).toHaveBeenCalledWith(1);
});
it('should exit with an error if the environment variable is empty', () => {
const consoleErrorSpy = spyOn(console, 'error');
const processExitSpy = spyOn(process, 'exit');
getEnvVar(emptyVar);
expect(consoleErrorSpy).toHaveBeenCalled();
expect(consoleErrorSpy.calls.argsFor(0)[0]).toContain(emptyVar);
expect(processExitSpy).toHaveBeenCalledWith(1);
});
it('should return an empty string if an undefined variable is optional', () => {
expect(getEnvVar(undefinedVar, true)).toBe('');
});
it('should return an empty string if an empty variable is optional', () => {
expect(getEnvVar(emptyVar, true)).toBe('');
});
});
describe('Logger', () => {
let consoleErrorSpy: jasmine.Spy;
let consoleInfoSpy: jasmine.Spy;
let consoleLogSpy: jasmine.Spy;
let consoleWarnSpy: jasmine.Spy;
let logger: Logger;
beforeEach(() => {
consoleErrorSpy = spyOn(console, 'error');
consoleInfoSpy = spyOn(console, 'info');
consoleLogSpy = spyOn(console, 'log');
consoleWarnSpy = spyOn(console, 'warn');
logger = new Logger('TestScope');
});
it('should delegate to `console`', () => {
logger.error('foo');
expect(consoleErrorSpy).toHaveBeenCalledTimes(1);
expect(consoleErrorSpy.calls.argsFor(0)).toContain('foo');
logger.info('bar');
expect(consoleInfoSpy).toHaveBeenCalledTimes(1);
expect(consoleInfoSpy.calls.argsFor(0)).toContain('bar');
logger.log('baz');
expect(consoleLogSpy).toHaveBeenCalledTimes(1);
expect(consoleLogSpy.calls.argsFor(0)).toContain('baz');
logger.warn('qux');
expect(consoleWarnSpy).toHaveBeenCalledTimes(1);
expect(consoleWarnSpy.calls.argsFor(0)).toContain('qux');
});
it('should prepend messages with the current date and logger\'s scope', () => {
const mockDate = new Date(1337);
const expectedDateStr = `[${mockDate}]`;
const expectedScopeStr = 'TestScope: ';
jasmine.clock().mockDate(mockDate);
jasmine.clock().withMock(() => {
logger.error();
logger.info();
logger.log();
logger.warn();
});
expect(consoleErrorSpy).toHaveBeenCalledWith(expectedDateStr, expectedScopeStr);
expect(consoleInfoSpy).toHaveBeenCalledWith(expectedDateStr, expectedScopeStr);
expect(consoleLogSpy).toHaveBeenCalledWith(expectedDateStr, expectedScopeStr);
expect(consoleWarnSpy).toHaveBeenCalledWith(expectedDateStr, expectedScopeStr);
});
it('should pass all arguments to `console`', () => {
const someString = jasmine.any(String);
logger.error('foo1', 'foo2');
expect(consoleErrorSpy).toHaveBeenCalledWith(someString, someString, 'foo1', 'foo2');
logger.info('bar1', 'bar2');
expect(consoleInfoSpy).toHaveBeenCalledWith(someString, someString, 'bar1', 'bar2');
logger.log('baz1', 'baz2');
expect(consoleLogSpy).toHaveBeenCalledWith(someString, someString, 'baz1', 'baz2');
logger.warn('qux1', 'qux2');
expect(consoleWarnSpy).toHaveBeenCalledWith(someString, someString, 'qux1', 'qux2');
});
});
});
@@ -1,6 +0,0 @@
// Imports
import {runTests} from '../lib/common/run-tests';
// Run
const specFiles = [`${__dirname}/**/*.spec.js`];
runTests(specFiles);
@@ -1,635 +0,0 @@
// 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 {SHORT_SHA_LEN} from '../../lib/common/constants';
import {Logger} from '../../lib/common/utils';
import {BuildCreator} from '../../lib/preview-server/build-creator';
import {ChangedPrVisibilityEvent, CreatedBuildEvent} from '../../lib/preview-server/build-events';
import {PreviewServerError} from '../../lib/preview-server/preview-error';
import {customAsyncMatchers} from './jasmine-custom-async-matchers';
// Tests
describe('BuildCreator', () => {
const pr = 9;
const sha = '9'.repeat(40);
const shortSha = sha.slice(0, SHORT_SHA_LEN);
const archive = 'snapshot.tar.gz';
const buildsDir = 'builds/dir';
const hiddenPrDir = path.join(buildsDir, `hidden--${pr}`);
const publicPrDir = path.join(buildsDir, `${pr}`);
const hiddenShaDir = path.join(hiddenPrDir, shortSha);
const publicShaDir = path.join(publicPrDir, shortSha);
let bc: BuildCreator;
beforeEach(() => jasmine.addAsyncMatchers(customAsyncMatchers));
beforeEach(() => bc = new BuildCreator(buildsDir));
describe('constructor()', () => {
it('should throw if \'buildsDir\' is missing or empty', () => {
expect(() => new BuildCreator('')).toThrowError('Missing or empty required parameter \'buildsDir\'!');
});
it('should extend EventEmitter', () => {
expect(bc).toBeInstanceOf(BuildCreator);
expect(bc).toBeInstanceOf(EventEmitter);
expect(Object.getPrototypeOf(bc)).toBe(BuildCreator.prototype);
});
});
describe('create()', () => {
let bcEmitSpy: jasmine.Spy;
let bcExistsSpy: jasmine.Spy;
let bcExtractArchiveSpy: jasmine.Spy;
let bcUpdatePrVisibilitySpy: jasmine.Spy;
let shellMkdirSpy: jasmine.Spy;
let shellRmSpy: jasmine.Spy;
beforeEach(() => {
bcEmitSpy = spyOn(bc, 'emit');
bcExistsSpy = spyOn(bc as any, 'exists');
bcExtractArchiveSpy = spyOn(bc as any, 'extractArchive');
bcUpdatePrVisibilitySpy = spyOn(bc, 'updatePrVisibility');
shellMkdirSpy = spyOn(shell, 'mkdir');
shellRmSpy = spyOn(shell, 'rm');
});
[true, false].forEach(isPublic => {
const prDir = isPublic ? publicPrDir : hiddenPrDir;
const shaDir = isPublic ? publicShaDir : hiddenShaDir;
it('should return a promise', async () => {
const promise = bc.create(pr, sha, archive, isPublic);
expect(promise).toBeInstanceOf(Promise);
// Do not complete the test (and release the spies) synchronously to avoid running the actual
// `extractArchive()`.
await promise;
});
it('should update the PR\'s visibility first if necessary', async () => {
await bc.create(pr, sha, archive, isPublic);
expect(bcUpdatePrVisibilitySpy).toHaveBeenCalledBefore(shellMkdirSpy);
expect(bcUpdatePrVisibilitySpy).toHaveBeenCalledWith(pr, isPublic);
expect(shellMkdirSpy).toHaveBeenCalled();
});
it('should create the build directory (and any parent directories)', async () => {
await bc.create(pr, sha, archive, isPublic);
expect(shellMkdirSpy).toHaveBeenCalledWith('-p', shaDir);
});
it('should extract the archive contents into the build directory', async () => {
await bc.create(pr, sha, archive, isPublic);
expect(bcExtractArchiveSpy).toHaveBeenCalledWith(archive, shaDir);
});
it('should emit a CreatedBuildEvent on success', async () => {
let emitted = false;
bcEmitSpy.and.callFake((type: string, evt: CreatedBuildEvent) => {
expect(type).toBe(CreatedBuildEvent.type);
expect(evt).toBeInstanceOf(CreatedBuildEvent);
expect(evt.pr).toBe(+pr);
expect(evt.sha).toBe(shortSha);
expect(evt.isPublic).toBe(isPublic);
emitted = true;
});
await bc.create(pr, sha, archive, isPublic);
expect(emitted).toBe(true);
});
describe('on error', () => {
beforeEach(() => {
bcExistsSpy.and.returnValue(false);
});
it('should abort and skip further operations if changing the PR\'s visibility fails', async () => {
const mockError = new PreviewServerError(543, 'Test');
bcUpdatePrVisibilitySpy.and.rejectWith(mockError);
await expectAsync(bc.create(pr, sha, archive, isPublic)).toBeRejectedWith(mockError);
expect(bcExistsSpy).not.toHaveBeenCalled();
expect(shellMkdirSpy).not.toHaveBeenCalled();
expect(bcExtractArchiveSpy).not.toHaveBeenCalled();
expect(bcEmitSpy).not.toHaveBeenCalled();
});
it('should abort and skip further operations if the build does already exist', async () => {
bcExistsSpy.withArgs(shaDir).and.returnValue(true);
await expectAsync(bc.create(pr, sha, archive, isPublic)).toBeRejectedWithPreviewServerError(
409, `Request to overwrite existing ${isPublic ? '' : 'non-'}public directory: ${shaDir}`);
expect(shellMkdirSpy).not.toHaveBeenCalled();
expect(bcExtractArchiveSpy).not.toHaveBeenCalled();
expect(bcEmitSpy).not.toHaveBeenCalled();
});
it('should detect existing build directory after visibility change', async () => {
bcUpdatePrVisibilitySpy.and.callFake(() => bcExistsSpy.and.returnValue(true));
expect(bcExistsSpy(prDir)).toBe(false);
expect(bcExistsSpy(shaDir)).toBe(false);
await expectAsync(bc.create(pr, sha, archive, isPublic)).toBeRejectedWithPreviewServerError(
409, `Request to overwrite existing ${isPublic ? '' : 'non-'}public directory: ${shaDir}`);
expect(shellMkdirSpy).not.toHaveBeenCalled();
expect(bcExtractArchiveSpy).not.toHaveBeenCalled();
expect(bcEmitSpy).not.toHaveBeenCalled();
});
it('should abort and skip further operations if it fails to create the directories', async () => {
shellMkdirSpy.and.throwError('');
await expectAsync(bc.create(pr, sha, archive, isPublic)).toBeRejected();
expect(shellMkdirSpy).toHaveBeenCalled();
expect(bcExtractArchiveSpy).not.toHaveBeenCalled();
expect(bcEmitSpy).not.toHaveBeenCalled();
});
it('should abort and skip further operations if it fails to extract the archive', async () => {
bcExtractArchiveSpy.and.throwError('');
await expectAsync(bc.create(pr, sha, archive, isPublic)).toBeRejected();
expect(shellMkdirSpy).toHaveBeenCalled();
expect(bcExtractArchiveSpy).toHaveBeenCalled();
expect(bcEmitSpy).not.toHaveBeenCalled();
});
it('should delete the PR directory (for new PR)', async () => {
bcExtractArchiveSpy.and.throwError('');
await expectAsync(bc.create(pr, sha, archive, isPublic)).toBeRejected();
expect(shellRmSpy).toHaveBeenCalledWith('-rf', prDir);
});
it('should delete the SHA directory (for existing PR)', async () => {
bcExistsSpy.withArgs(prDir).and.returnValue(true);
bcExtractArchiveSpy.and.throwError('');
await expectAsync(bc.create(pr, sha, archive, isPublic)).toBeRejected();
expect(shellRmSpy).toHaveBeenCalledWith('-rf', shaDir);
});
it('should reject with an PreviewServerError', async () => {
// tslint:disable-next-line: no-string-throw
shellMkdirSpy.and.callFake(() => { throw 'Test'; });
await expectAsync(bc.create(pr, sha, archive, isPublic)).toBeRejectedWithPreviewServerError(
500, `Error while creating preview at: ${shaDir}\nTest`);
});
it('should pass PreviewServerError instances unmodified', async () => {
shellMkdirSpy.and.callFake(() => { throw new PreviewServerError(543, 'Test'); });
await expectAsync(bc.create(pr, sha, archive, isPublic)).toBeRejectedWithPreviewServerError(543, 'Test');
});
});
});
});
describe('updatePrVisibility()', () => {
let bcEmitSpy: jasmine.Spy;
let bcExistsSpy: jasmine.Spy;
let bcListShasByDate: jasmine.Spy;
let shellMvSpy: jasmine.Spy;
beforeEach(() => {
bcEmitSpy = spyOn(bc, 'emit');
bcExistsSpy = spyOn(bc as any, 'exists');
bcListShasByDate = spyOn(bc as any, 'listShasByDate');
shellMvSpy = spyOn(shell, 'mv');
bcExistsSpy.and.returnValues(Promise.resolve(true), Promise.resolve(false));
bcListShasByDate.and.returnValue([]);
});
it('should return a promise', async () => {
const promise = bc.updatePrVisibility(pr, true);
expect(promise).toBeInstanceOf(Promise);
// Do not complete the test (and release the spies) synchronously to avoid running the actual `extractArchive()`.
await promise;
});
[true, false].forEach(makePublic => {
const oldPrDir = makePublic ? hiddenPrDir : publicPrDir;
const newPrDir = makePublic ? publicPrDir : hiddenPrDir;
it('should rename the directory', async () => {
await bc.updatePrVisibility(pr, makePublic);
expect(shellMvSpy).toHaveBeenCalledWith(oldPrDir, newPrDir);
});
describe('when the visibility is updated', () => {
it('should resolve to true', async () => {
await expectAsync(bc.updatePrVisibility(pr, makePublic)).toBeResolvedTo(true);
});
it('should rename the directory', async () => {
await bc.updatePrVisibility(pr, makePublic);
expect(shellMvSpy).toHaveBeenCalledWith(oldPrDir, newPrDir);
});
it('should emit a ChangedPrVisibilityEvent on success', async () => {
let emitted = false;
bcEmitSpy.and.callFake((type: string, evt: ChangedPrVisibilityEvent) => {
expect(type).toBe(ChangedPrVisibilityEvent.type);
expect(evt).toBeInstanceOf(ChangedPrVisibilityEvent);
expect(evt.pr).toBe(+pr);
expect(evt.shas).toBeInstanceOf(Array);
expect(evt.isPublic).toBe(makePublic);
emitted = true;
});
await bc.updatePrVisibility(pr, makePublic);
expect(emitted).toBe(true);
});
it('should include all shas in the emitted event', async () => {
const shas = ['foo', 'bar', 'baz'];
let emitted = false;
bcListShasByDate.and.resolveTo(shas);
bcEmitSpy.and.callFake((type: string, evt: ChangedPrVisibilityEvent) => {
expect(bcListShasByDate).toHaveBeenCalledWith(newPrDir);
expect(type).toBe(ChangedPrVisibilityEvent.type);
expect(evt).toBeInstanceOf(ChangedPrVisibilityEvent);
expect(evt.pr).toBe(+pr);
expect(evt.shas).toBe(shas);
expect(evt.isPublic).toBe(makePublic);
emitted = true;
});
await bc.updatePrVisibility(pr, makePublic);
expect(emitted).toBe(true);
});
});
it('should do nothing if the visibility is already up-to-date', async () => {
bcExistsSpy.and.callFake((dir: string) => dir === newPrDir);
await expectAsync(bc.updatePrVisibility(pr, makePublic)).toBeResolvedTo(false);
expect(shellMvSpy).not.toHaveBeenCalled();
expect(bcListShasByDate).not.toHaveBeenCalled();
expect(bcEmitSpy).not.toHaveBeenCalled();
});
it('should do nothing if the PR directory does not exist', async () => {
bcExistsSpy.and.returnValue(false);
await expectAsync(bc.updatePrVisibility(pr, makePublic)).toBeResolvedTo(false);
expect(shellMvSpy).not.toHaveBeenCalled();
expect(bcListShasByDate).not.toHaveBeenCalled();
expect(bcEmitSpy).not.toHaveBeenCalled();
});
describe('on error', () => {
it('should abort and skip further operations if both directories exist', async () => {
bcExistsSpy.and.returnValue(true);
await expectAsync(bc.updatePrVisibility(pr, makePublic)).toBeRejectedWithPreviewServerError(
409, `Request to move '${oldPrDir}' to existing directory '${newPrDir}'.`);
expect(shellMvSpy).not.toHaveBeenCalled();
expect(bcListShasByDate).not.toHaveBeenCalled();
expect(bcEmitSpy).not.toHaveBeenCalled();
});
it('should abort and skip further operations if it fails to rename the directory', async () => {
shellMvSpy.and.throwError('');
await expectAsync(bc.updatePrVisibility(pr, makePublic)).toBeRejected();
expect(shellMvSpy).toHaveBeenCalled();
expect(bcListShasByDate).not.toHaveBeenCalled();
expect(bcEmitSpy).not.toHaveBeenCalled();
});
it('should abort and skip further operations if it fails to list the SHAs', async () => {
bcListShasByDate.and.throwError('');
await expectAsync(bc.updatePrVisibility(pr, makePublic)).toBeRejected();
expect(shellMvSpy).toHaveBeenCalled();
expect(bcListShasByDate).toHaveBeenCalled();
expect(bcEmitSpy).not.toHaveBeenCalled();
});
it('should reject with an PreviewServerError', async () => {
// tslint:disable-next-line: no-string-throw
shellMvSpy.and.callFake(() => { throw 'Test'; });
await expectAsync(bc.updatePrVisibility(pr, makePublic)).toBeRejectedWithPreviewServerError(
500, `Error while making PR ${pr} ${makePublic ? 'public' : 'hidden'}.\nTest`);
});
it('should pass PreviewServerError instances unmodified', async () => {
shellMvSpy.and.callFake(() => { throw new PreviewServerError(543, 'Test'); });
await expectAsync(bc.updatePrVisibility(pr, makePublic)).toBeRejectedWithPreviewServerError(543, 'Test');
});
});
});
});
// Protected methods
describe('exists()', () => {
let fsAccessSpy: jasmine.Spy;
let fsAccessCbs: ((v?: any) => void)[];
beforeEach(() => {
fsAccessCbs = [];
fsAccessSpy = spyOn(fs, 'access').and.callFake(
((_: string, cb: (v?: any) => void) => fsAccessCbs.push(cb)) as unknown as typeof fs.access,
);
});
it('should return a promise', () => {
expect((bc as any).exists('foo')).toBeInstanceOf(Promise);
});
it('should call \'fs.access()\' with the specified argument', () => {
(bc as any).exists('foo');
expect(fsAccessSpy).toHaveBeenCalledWith('foo', jasmine.any(Function));
});
it('should resolve with \'true\' if \'fs.access()\' succeeds', async () => {
const existsPromises = [
(bc as any).exists('foo'),
(bc as any).exists('bar'),
];
fsAccessCbs[0]();
fsAccessCbs[1](null);
await expectAsync(Promise.all(existsPromises)).toBeResolvedTo([true, true]);
});
it('should resolve with \'false\' if \'fs.access()\' errors', async () => {
const existsPromises = [
(bc as any).exists('foo'),
(bc as any).exists('bar'),
];
fsAccessCbs[0]('Error');
fsAccessCbs[1](new Error());
await expectAsync(Promise.all(existsPromises)).toBeResolvedTo([false, false]);
});
});
describe('extractArchive()', () => {
let consoleWarnSpy: jasmine.Spy;
let shellChmodSpy: jasmine.Spy;
let shellRmSpy: jasmine.Spy;
let cpExecSpy: jasmine.Spy;
let cpExecCbs: ((...args: any[]) => void)[];
beforeEach(() => {
cpExecCbs = [];
consoleWarnSpy = spyOn(Logger.prototype, 'warn');
shellChmodSpy = spyOn(shell, 'chmod');
shellRmSpy = spyOn(shell, 'rm');
cpExecSpy = spyOn(cp, 'exec').and.callFake(
((_: string, cb: (...args: any[]) => void) =>
cpExecCbs.push(cb)) as unknown as typeof cp.exec,
);
});
it('should return a promise', () => {
expect((bc as any).extractArchive('foo', 'bar')).toBeInstanceOf(Promise);
});
it('should "gunzip" and "untar" the input file into the output directory', () => {
const cmd = 'tar --extract --gzip --directory "output/dir" --file "input/file"';
(bc as any).extractArchive('input/file', 'output/dir');
expect(cpExecSpy).toHaveBeenCalledWith(cmd, jasmine.any(Function));
});
it('should log (as a warning) any stderr output if extracting succeeded', async () => {
const extractPromise = (bc as any).extractArchive('foo', 'bar');
cpExecCbs[0](null, 'This is stdout', 'This is stderr');
await expectAsync(extractPromise).toBeResolved();
expect(consoleWarnSpy).toHaveBeenCalledWith('This is stderr');
});
it('should make the build directory non-writable', async () => {
const extractPromise = (bc as any).extractArchive('foo', 'bar');
cpExecCbs[0]();
await expectAsync(extractPromise).toBeResolved();
expect(shellChmodSpy).toHaveBeenCalledWith('-R', 'a-w', 'bar');
});
it('should delete the build artifact file on success', async () => {
const extractPromise = (bc as any).extractArchive('input/file', 'output/dir');
cpExecCbs[0]();
await expectAsync(extractPromise).toBeResolved();
expect(shellRmSpy).toHaveBeenCalledWith('-f', 'input/file');
});
describe('on error', () => {
it('should abort and skip further operations if it fails to extract the archive', async () => {
const extractPromise = (bc as any).extractArchive('foo', 'bar');
cpExecCbs[0]('Test');
await expectAsync(extractPromise).toBeRejectedWith('Test');
expect(shellChmodSpy).not.toHaveBeenCalled();
expect(shellRmSpy).not.toHaveBeenCalled();
});
it('should abort and skip further operations if it fails to make non-writable', async () => {
// tslint:disable-next-line: no-string-throw
shellChmodSpy.and.callFake(() => { throw 'Test'; });
const extractPromise = (bc as any).extractArchive('foo', 'bar');
cpExecCbs[0]();
await expectAsync(extractPromise).toBeRejectedWith('Test');
expect(shellChmodSpy).toHaveBeenCalled();
expect(shellRmSpy).not.toHaveBeenCalled();
});
it('should abort and reject if it fails to remove the build artifact file', async () => {
// tslint:disable-next-line: no-string-throw
shellRmSpy.and.callFake(() => { throw 'Test'; });
const extractPromise = (bc as any).extractArchive('foo', 'bar');
cpExecCbs[0]();
await expectAsync(extractPromise).toBeRejectedWith('Test');
expect(shellChmodSpy).toHaveBeenCalled();
expect(shellRmSpy).toHaveBeenCalled();
});
});
});
describe('listShasByDate()', () => {
let shellLsSpy: jasmine.Spy;
const lsResult = (name: string, mtimeMs: number, isDirectory = true) => ({
isDirectory: () => isDirectory,
mtime: new Date(mtimeMs),
name,
});
beforeEach(() => {
shellLsSpy = spyOn(shell, 'ls').and.returnValue([] as unknown as shell.ShellArray);
});
it('should return a promise', async () => {
const promise = (bc as any).listShasByDate('input/dir');
expect(promise).toBeInstanceOf(Promise);
// Do not complete the test (and release the spies) synchronously to avoid running the actual `ls()`.
await promise;
});
it('should `ls()` files with their metadata', async () => {
await (bc as any).listShasByDate('input/dir');
expect(shellLsSpy).toHaveBeenCalledWith('-l', 'input/dir');
});
it('should reject if listing files fails', async () => {
shellLsSpy.and.rejectWith('Test');
await expectAsync((bc as any).listShasByDate('input/dir')).toBeRejectedWith('Test');
});
it('should return the filenames', async () => {
shellLsSpy.and.resolveTo([
lsResult('foo', 100),
lsResult('bar', 200),
lsResult('baz', 300),
]);
await expectAsync((bc as any).listShasByDate('input/dir')).toBeResolvedTo(['foo', 'bar', 'baz']);
});
it('should sort by date', async () => {
shellLsSpy.and.resolveTo([
lsResult('foo', 300),
lsResult('bar', 100),
lsResult('baz', 200),
]);
await expectAsync((bc as any).listShasByDate('input/dir')).toBeResolvedTo(['bar', 'baz', 'foo']);
});
it('should not break with ShellJS\' custom `sort()` method', async () => {
const mockArray = [
lsResult('foo', 300),
lsResult('bar', 100),
lsResult('baz', 200),
];
mockArray.sort = jasmine.createSpy('sort');
shellLsSpy.and.resolveTo(mockArray);
await expectAsync((bc as any).listShasByDate('input/dir')).toBeResolvedTo(['bar', 'baz', 'foo']);
expect(mockArray.sort).not.toHaveBeenCalled();
});
it('should only include directories', async () => {
shellLsSpy.and.resolveTo([
lsResult('foo', 100),
lsResult('bar', 200, false),
lsResult('baz', 300),
]);
await expectAsync((bc as any).listShasByDate('input/dir')).toBeResolvedTo(['foo', 'baz']);
});
});
});
@@ -1,58 +0,0 @@
// Imports
import {ChangedPrVisibilityEvent, CreatedBuildEvent} from '../../lib/preview-server/build-events';
// Tests
describe('ChangedPrVisibilityEvent', () => {
let evt: ChangedPrVisibilityEvent;
beforeEach(() => evt = new ChangedPrVisibilityEvent(42, ['foo', 'bar'], true));
it('should have a static \'type\' property', () => {
expect(ChangedPrVisibilityEvent.type).toBe('pr.changedVisibility');
});
it('should have a \'pr\' property', () => {
expect(evt.pr).toBe(42);
});
it('should have a \'shas\' property', () => {
expect(evt.shas).toEqual(['foo', 'bar']);
});
it('should have an \'isPublic\' property', () => {
expect(evt.isPublic).toBe(true);
});
});
describe('CreatedBuildEvent', () => {
let evt: CreatedBuildEvent;
beforeEach(() => evt = new CreatedBuildEvent(42, 'bar', true));
it('should have a static \'type\' property', () => {
expect(CreatedBuildEvent.type).toBe('build.created');
});
it('should have a \'pr\' property', () => {
expect(evt.pr).toBe(42);
});
it('should have a \'sha\' property', () => {
expect(evt.sha).toBe('bar');
});
it('should have an \'isPublic\' property', () => {
expect(evt.isPublic).toBe(true);
});
});
@@ -1,178 +0,0 @@
import * as fs from 'fs';
import * as nock from 'nock';
import {resolve as resolvePath} from 'path';
import {BuildInfo, CircleCiApi, PipelineInfo} from '../../lib/common/circle-ci-api';
import {Logger} from '../../lib/common/utils';
import {BuildRetriever} from '../../lib/preview-server/build-retriever';
describe('BuildRetriever', () => {
const MAX_DOWNLOAD_SIZE = 10000;
const DOWNLOAD_DIR = resolvePath('/DOWNLOAD/DIR');
const BASE_URL = 'http://test.com';
const ARTIFACT_PATH = '/some/path/build.zip';
let api: CircleCiApi;
let BUILD_INFO: BuildInfo;
let PIPELINE_INFO: PipelineInfo;
let WRITEFILE_RESULT: any;
let writeFileSpy: jasmine.Spy;
let EXISTS_RESULT: boolean;
let existsSpy: jasmine.Spy;
let getBuildArtifactUrlSpy: jasmine.Spy;
beforeEach(() => {
BUILD_INFO = {
number: 12345,
name: 'test_job',
organization: {name: 'ORG'},
project: {name: 'REPO'},
pipeline: {id: 'test_pipeline'},
};
PIPELINE_INFO = {
id: 'test_pipeline',
vcs: {
review_id: '777',
revision: 'COMMIT',
},
};
api = new CircleCiApi('ORG', 'REPO', 'TOKEN');
spyOn(api, 'getBuildInfo').and.resolveTo(BUILD_INFO);
spyOn(api, 'getPipelineInfo').and.resolveTo(PIPELINE_INFO);
getBuildArtifactUrlSpy = spyOn(api, 'getBuildArtifactUrl').and.resolveTo(BASE_URL + ARTIFACT_PATH);
WRITEFILE_RESULT = undefined;
writeFileSpy = spyOn(fs, 'writeFile').and.callFake(
((_path: string, _buffer: Buffer, callback: fs.NoParamCallback) =>
callback(WRITEFILE_RESULT)) as typeof fs.writeFile,
);
EXISTS_RESULT = false;
existsSpy = spyOn(fs, 'exists').and.callFake(
((_path, callback) => callback(EXISTS_RESULT)) as typeof fs.exists,
);
});
describe('constructor', () => {
it('should fail if the "downloadSizeLimit" is invalid', () => {
expect(() => new BuildRetriever(api, NaN, DOWNLOAD_DIR))
.toThrowError(`Invalid parameter "downloadSizeLimit" should be a number greater than 0.`);
expect(() => new BuildRetriever(api, 0, DOWNLOAD_DIR))
.toThrowError(`Invalid parameter "downloadSizeLimit" should be a number greater than 0.`);
expect(() => new BuildRetriever(api, -1, DOWNLOAD_DIR))
.toThrowError(`Invalid parameter "downloadSizeLimit" should be a number greater than 0.`);
});
it('should fail if the "downloadDir" is missing', () => {
expect(() => new BuildRetriever(api, MAX_DOWNLOAD_SIZE, ''))
.toThrowError(`Missing or empty required parameter 'downloadDir'!`);
});
});
describe('getGithubInfo', () => {
it('should request the build and pipeline info from CircleCI', async () => {
const retriever = new BuildRetriever(api, MAX_DOWNLOAD_SIZE, DOWNLOAD_DIR);
const info = await retriever.getGithubInfo(12345);
expect(api.getBuildInfo).toHaveBeenCalledWith(12345);
expect(api.getPipelineInfo).toHaveBeenCalledWith('test_pipeline');
expect(info).toEqual({org: 'ORG', pr: 777, repo: 'REPO', sha: 'COMMIT'});
});
});
describe('downloadBuildArtifact', () => {
const ARTIFACT_CONTENTS = 'ARTIFACT CONTENTS';
let retriever: BuildRetriever;
beforeEach(() => {
spyOn(Logger.prototype, 'warn');
retriever = new BuildRetriever(api, MAX_DOWNLOAD_SIZE, DOWNLOAD_DIR);
});
it('should get the artifact URL from the CircleCI API', async () => {
const artifactRequest = nock(BASE_URL).get(ARTIFACT_PATH).reply(200, ARTIFACT_CONTENTS);
await retriever.downloadBuildArtifact(12345, 777, 'COMMIT', ARTIFACT_PATH);
expect(api.getBuildArtifactUrl).toHaveBeenCalledWith(12345, ARTIFACT_PATH);
artifactRequest.done();
});
it('should download the artifact from its URL', async () => {
const artifactRequest = nock(BASE_URL).get(ARTIFACT_PATH).reply(200, ARTIFACT_CONTENTS);
await retriever.downloadBuildArtifact(12345, 777, 'COMMIT', ARTIFACT_PATH);
// The following line proves that the artifact URL fetch occurred.
artifactRequest.done();
});
it('should fail if the artifact is too large', async () => {
const artifactRequest = nock(BASE_URL).get(ARTIFACT_PATH).reply(200, ARTIFACT_CONTENTS);
retriever = new BuildRetriever(api, 10, DOWNLOAD_DIR);
await expectAsync(retriever.downloadBuildArtifact(12345, 777, 'COMMIT', ARTIFACT_PATH)).
toBeRejectedWith(jasmine.objectContaining({status: 413}));
artifactRequest.done();
});
it('should not download the artifact if it already exists', async () => {
const artifactRequestInterceptor = nock(BASE_URL).get(ARTIFACT_PATH);
const artifactRequest = artifactRequestInterceptor.reply(200, ARTIFACT_CONTENTS);
EXISTS_RESULT = true;
await retriever.downloadBuildArtifact(12345, 777, 'COMMIT', ARTIFACT_PATH);
expect(existsSpy).toHaveBeenCalled();
expect(getBuildArtifactUrlSpy).not.toHaveBeenCalled();
expect(artifactRequest.isDone()).toEqual(false);
nock.removeInterceptor(artifactRequestInterceptor);
});
it('should write the artifact file to disk', async () => {
const artifactRequest = nock(BASE_URL).get(ARTIFACT_PATH).reply(200, ARTIFACT_CONTENTS);
const downloadPath = resolvePath(`${DOWNLOAD_DIR}/777-COMMIT-build.zip`);
await retriever.downloadBuildArtifact(12345, 777, 'COMMIT', ARTIFACT_PATH);
expect(writeFileSpy).toHaveBeenCalledWith(downloadPath, jasmine.any(Buffer), jasmine.any(Function));
const buffer: Buffer = writeFileSpy.calls.mostRecent().args[1];
expect(buffer.toString()).toEqual(ARTIFACT_CONTENTS);
artifactRequest.done();
});
it('should fail if the CircleCI API fails', async () => {
getBuildArtifactUrlSpy.and.rejectWith('getBuildArtifactUrl failed');
await expectAsync(retriever.downloadBuildArtifact(12345, 777, 'COMMIT', ARTIFACT_PATH)).
toBeRejectedWithError('CircleCI artifact download failed (getBuildArtifactUrl failed)');
});
it('should fail if the URL fetch errors', async () => {
// create a new handler that errors
const artifactRequest = nock(BASE_URL).get(ARTIFACT_PATH).replyWithError('Artifact Request Failed');
await expectAsync(retriever.downloadBuildArtifact(12345, 777, 'COMMIT', ARTIFACT_PATH)).toBeRejectedWithError(
'CircleCI artifact download failed ' +
'(request to http://test.com/some/path/build.zip failed, reason: Artifact Request Failed)');
artifactRequest.done();
});
it('should fail if the URL fetch 404s', async () => {
// create a new handler that errors
const artifactRequest = nock(BASE_URL).get(ARTIFACT_PATH).reply(404, 'No such artifact');
await expectAsync(retriever.downloadBuildArtifact(12345, 777, 'COMMIT', ARTIFACT_PATH)).
toBeRejectedWithError(`CircleCI artifact download failed (${BASE_URL}/some/path/build.zip: 404 - Not Found)`);
artifactRequest.done();
});
it('should fail if file write fails', async () => {
const artifactRequest = nock(BASE_URL).get(ARTIFACT_PATH).reply(200, ARTIFACT_CONTENTS);
WRITEFILE_RESULT = 'Test Error';
await expectAsync(retriever.downloadBuildArtifact(12345, 777, 'COMMIT', ARTIFACT_PATH)).
toBeRejectedWithError('CircleCI artifact download failed (Test Error)');
artifactRequest.done();
});
});
});
@@ -1,158 +0,0 @@
// Imports
import {GithubApi} from '../../lib/common/github-api';
import {GithubPullRequests, PullRequest} from '../../lib/common/github-pull-requests';
import {GithubTeams} from '../../lib/common/github-teams';
import {BuildVerifier} from '../../lib/preview-server/build-verifier';
// Tests
describe('BuildVerifier', () => {
const defaultConfig = {
allowedTeamSlugs: ['team1', 'team2'],
githubOrg: 'organization',
githubRepo: 'repo',
githubToken: 'githubToken',
secret: 'secret',
trustedPrLabel: 'trusted: pr-label',
};
let prs: GithubPullRequests;
let bv: BuildVerifier;
// Helpers
const createBuildVerifier = (partialConfig: Partial<typeof defaultConfig> = {}) => {
const cfg = {...defaultConfig, ...partialConfig} as typeof defaultConfig;
const api = new GithubApi(cfg.githubToken);
prs = new GithubPullRequests(api, cfg.githubOrg, cfg.githubRepo);
const teams = new GithubTeams(api, cfg.githubOrg);
return new BuildVerifier(prs, teams, cfg.allowedTeamSlugs, cfg.trustedPrLabel);
};
beforeEach(() => bv = createBuildVerifier());
describe('constructor()', () => {
['githubToken', 'githubRepo', 'githubOrg', 'allowedTeamSlugs', 'trustedPrLabel'].
forEach(param => {
it(`should throw if '${param}' is missing or empty`, () => {
expect(() => createBuildVerifier({[param]: ''})).
toThrowError(`Missing or empty required parameter '${param}'!`);
});
});
it('should throw if \'allowedTeamSlugs\' is an empty array', () => {
expect(() => createBuildVerifier({allowedTeamSlugs: []})).
toThrowError('Missing or empty required parameter \'allowedTeamSlugs\'!');
});
});
describe('getSignificantFilesChanged', () => {
it('should return false if none of the fetched files match the given pattern', async () => {
const fetchFilesSpy = spyOn(prs, 'fetchFiles');
fetchFilesSpy.and.resolveTo([
{filename: 'a/b/c', sha: 'a1'},
{filename: 'd/e/f', sha: 'b2'},
]);
expect(await bv.getSignificantFilesChanged(777, /^x/)).toEqual(false);
expect(fetchFilesSpy).toHaveBeenCalledWith(777);
fetchFilesSpy.calls.reset();
expect(await bv.getSignificantFilesChanged(777, /^a/)).toEqual(true);
expect(fetchFilesSpy).toHaveBeenCalledWith(777);
});
});
describe('getPrIsTrusted()', () => {
const pr = 9;
let mockPrInfo: PullRequest;
let prsFetchSpy: jasmine.Spy;
let teamsIsMemberBySlugSpy: jasmine.Spy;
beforeEach(() => {
mockPrInfo = {
labels: [
{name: 'foo'},
{name: 'bar'},
],
number: 9,
user: {login: 'username'},
};
prsFetchSpy = spyOn(GithubPullRequests.prototype, 'fetch').and.resolveTo(mockPrInfo);
teamsIsMemberBySlugSpy = spyOn(GithubTeams.prototype, 'isMemberBySlug').and.resolveTo(true);
});
it('should return a promise', async () => {
const promise = bv.getPrIsTrusted(pr);
expect(promise).toBeInstanceOf(Promise);
// Do not complete the test (and release the spies) synchronously to avoid running the actual
// `GithubTeams#isMemberBySlug()`.
await promise;
});
it('should fetch the corresponding PR', async () => {
await bv.getPrIsTrusted(pr);
expect(prsFetchSpy).toHaveBeenCalledWith(pr);
});
it('should fail if fetching the PR errors', async () => {
prsFetchSpy.and.rejectWith('Test');
await expectAsync(bv.getPrIsTrusted(pr)).toBeRejectedWith('Test');
});
describe('when the PR has the "trusted PR" label', () => {
beforeEach(() => mockPrInfo.labels.push({name: 'trusted: pr-label'}));
it('should resolve to true', async () => {
await expectAsync(bv.getPrIsTrusted(pr)).toBeResolvedTo(true);
});
it('should not try to verify the author\'s membership status', async () => {
await expectAsync(bv.getPrIsTrusted(pr));
expect(teamsIsMemberBySlugSpy).not.toHaveBeenCalled();
});
});
describe('when the PR does not have the "trusted PR" label', () => {
it('should verify the PR author\'s membership in the specified teams', async () => {
await bv.getPrIsTrusted(pr);
expect(teamsIsMemberBySlugSpy).toHaveBeenCalledWith('username', ['team1', 'team2']);
});
it('should fail if verifying membership errors', async () => {
teamsIsMemberBySlugSpy.and.rejectWith('Test');
await expectAsync(bv.getPrIsTrusted(pr)).toBeRejectedWith('Test');
});
it('should resolve to true if the PR\'s author is a member', async () => {
teamsIsMemberBySlugSpy.and.resolveTo(true);
await expectAsync(bv.getPrIsTrusted(pr)).toBeResolvedTo(true);
});
it('should resolve to false if the PR\'s author is not a member', async () => {
teamsIsMemberBySlugSpy.and.resolveTo(false);
await expectAsync(bv.getPrIsTrusted(pr)).toBeResolvedTo(false);
});
});
});
});
@@ -1,5 +0,0 @@
declare module jasmine {
interface AsyncMatchers {
toBeRejectedWithPreviewServerError(status: number, message?: string | RegExp): Promise<void>;
}
}
@@ -1,59 +0,0 @@
import {PreviewServerError} from '../../lib/preview-server/preview-error';
// Matchers
const toBeRejectedWithPreviewServerError: jasmine.CustomAsyncMatcherFactory = matchersUtil => {
return {
async compare(actualPromise: Promise<never>, expectedStatus: number, expectedMessage?: string | RegExp) {
if (!(actualPromise instanceof Promise)) {
throw new Error(`Expected '${toBeRejectedWithPreviewServerError.name}()' to be called on a promise.`);
}
try {
await actualPromise;
return {
pass: false,
message: `Expected a promise to be rejected with a '${PreviewServerError.name}', but it was resolved.`,
};
} catch (actualError) {
const actualPrintValue = stringify(actualError);
const expectedPrintValue =
stringify(new PreviewServerError(expectedStatus, expectedMessage && `${expectedMessage}`));
const pass = errorMatches(actualError, expectedStatus, expectedMessage);
const message =
`Expected a promise ${pass ? 'not ' : ''}to be rejected with ${expectedPrintValue}, but is was` +
`${pass ? '' : ` rejected with ${actualPrintValue}`}.`;
return {pass, message};
}
},
};
// Helpers
function errorMatches(actualErr: unknown, expectedStatus: number, expectedMsg?: string | RegExp): boolean {
if (!(actualErr instanceof PreviewServerError)) return false;
if (actualErr.status !== expectedStatus) return false;
return messageMatches(actualErr.message, expectedMsg);
}
function messageMatches(actualMsg: string, expectedMsg?: string | RegExp): boolean {
if (typeof expectedMsg === 'undefined') return true;
if (typeof expectedMsg === 'string') return actualMsg === expectedMsg;
return expectedMsg.test(actualMsg);
}
function stringify(value: unknown): string {
if (value instanceof PreviewServerError) {
return `${PreviewServerError.name}(${value.status}${value.message ? `, ${value.message}` : ''})`;
}
return matchersUtil.pp(value);
}
};
// Exports
export const customAsyncMatchers: jasmine.CustomAsyncMatcherFactories = {
toBeRejectedWithPreviewServerError,
};
@@ -1,39 +0,0 @@
// Imports
import {PreviewServerError} from '../../lib/preview-server/preview-error';
// Tests
describe('PreviewServerError', () => {
let err: PreviewServerError;
beforeEach(() => err = new PreviewServerError(999, 'message'));
it('should extend Error', () => {
expect(err).toBeInstanceOf(PreviewServerError);
expect(err).toBeInstanceOf(Error);
expect(Object.getPrototypeOf(err)).toBe(PreviewServerError.prototype);
});
it('should have a \'status\' property', () => {
expect(err.status).toBe(999);
});
it('should have a \'message\' property', () => {
expect(err.message).toBe('message');
});
it('should have a 500 \'status\' by default', () => {
expect(new PreviewServerError().status).toBe(500);
});
it('should have an empty \'message\' by default', () => {
expect(new PreviewServerError().message).toBe('');
expect(new PreviewServerError(999).message).toBe('');
});
});
@@ -1,687 +0,0 @@
// Imports
import * as http from 'http';
import * as supertest from 'supertest';
import {CircleCiApi} from '../../lib/common/circle-ci-api';
import {GithubApi} from '../../lib/common/github-api';
import {GithubPullRequests} from '../../lib/common/github-pull-requests';
import {GithubTeams} from '../../lib/common/github-teams';
import {Logger} from '../../lib/common/utils';
import {BuildCreator} from '../../lib/preview-server/build-creator';
import {ChangedPrVisibilityEvent, CreatedBuildEvent} from '../../lib/preview-server/build-events';
import {BuildRetriever, GithubInfo} from '../../lib/preview-server/build-retriever';
import {BuildVerifier} from '../../lib/preview-server/build-verifier';
import {PreviewServerConfig, PreviewServerFactory} from '../../lib/preview-server/preview-server-factory';
interface CircleCiWebHookPayload {
payload: {
build_num: number;
build_parameters: {
CIRCLE_JOB: string;
}
};
}
// Tests
describe('PreviewServerFactory', () => {
const defaultConfig: PreviewServerConfig = {
buildArtifactPath: 'artifact/path.zip',
buildsDir: 'builds/dir',
circleCiToken: 'CIRCLE_CI_TOKEN',
domainName: 'domain.name',
downloadSizeLimit: 999,
downloadsDir: '/tmp/aio-create-builds',
githubOrg: 'organisation',
githubRepo: 'repo',
githubTeamSlugs: ['team1', 'team2'],
githubToken: '12345',
significantFilesPattern: '^(?:aio|packages)\\/(?!.*[._]spec\\.[jt]s$)',
trustedPrLabel: 'trusted: pr-label',
};
let loggerErrorSpy: jasmine.Spy;
let loggerInfoSpy: jasmine.Spy;
let loggerLogSpy: jasmine.Spy;
// Helpers
const createPreviewServer = (partialConfig: Partial<PreviewServerConfig> = {}) =>
PreviewServerFactory.create({...defaultConfig, ...partialConfig});
beforeEach(() => {
loggerErrorSpy = spyOn(Logger.prototype, 'error');
loggerInfoSpy = spyOn(Logger.prototype, 'info');
loggerLogSpy = spyOn(Logger.prototype, 'log');
});
describe('create()', () => {
let usfCreateMiddlewareSpy: jasmine.Spy;
beforeEach(() => {
usfCreateMiddlewareSpy = spyOn(PreviewServerFactory, 'createMiddleware').and.callThrough();
});
it('should throw if \'buildsDir\' is missing or empty', () => {
expect(() => createPreviewServer({buildsDir: ''})).
toThrowError('Missing or empty required parameter \'buildsDir\'!');
});
it('should throw if \'domainName\' is missing or empty', () => {
expect(() => createPreviewServer({domainName: ''})).
toThrowError('Missing or empty required parameter \'domainName\'!');
});
it('should throw if \'githubToken\' is missing or empty', () => {
expect(() => createPreviewServer({githubToken: ''})).
toThrowError('Missing or empty required parameter \'githubToken\'!');
});
it('should throw if \'githubOrg\' is missing or empty', () => {
expect(() => createPreviewServer({githubOrg: ''})).
toThrowError('Missing or empty required parameter \'githubOrg\'!');
});
it('should throw if \'githubTeamSlugs\' is missing or empty', () => {
expect(() => createPreviewServer({githubTeamSlugs: []})).
toThrowError('Missing or empty required parameter \'allowedTeamSlugs\'!');
});
it('should throw if \'githubRepo\' is missing or empty', () => {
expect(() => createPreviewServer({githubRepo: ''})).
toThrowError('Missing or empty required parameter \'githubRepo\'!');
});
it('should throw if \'trustedPrLabel\' is missing or empty', () => {
expect(() => createPreviewServer({trustedPrLabel: ''})).
toThrowError('Missing or empty required parameter \'trustedPrLabel\'!');
});
it('should return an http.Server', () => {
const httpCreateServerSpy = spyOn(http, 'createServer').and.callThrough();
const server = createPreviewServer();
expect(server).toBe(httpCreateServerSpy.calls.mostRecent().returnValue);
});
it('should create and use an appropriate BuildCreator', () => {
const usfCreateBuildCreatorSpy = spyOn(PreviewServerFactory, 'createBuildCreator').and.callThrough();
createPreviewServer();
const buildRetriever = jasmine.any(BuildRetriever);
const buildVerifier = jasmine.any(BuildVerifier);
const prs = jasmine.any(GithubPullRequests);
const buildCreator: BuildCreator = usfCreateBuildCreatorSpy.calls.mostRecent().returnValue;
expect(usfCreateMiddlewareSpy).toHaveBeenCalledWith(buildRetriever, buildVerifier, buildCreator, defaultConfig);
expect(usfCreateBuildCreatorSpy).toHaveBeenCalledWith(prs, 'builds/dir', 'domain.name');
});
it('should create and use an appropriate middleware', () => {
const httpCreateServerSpy = spyOn(http, 'createServer').and.callThrough();
createPreviewServer();
const buildRetriever = jasmine.any(BuildRetriever);
const buildVerifier = jasmine.any(BuildVerifier);
const buildCreator = jasmine.any(BuildCreator);
expect(usfCreateMiddlewareSpy).toHaveBeenCalledWith(buildRetriever, buildVerifier, buildCreator, defaultConfig);
const middleware = usfCreateMiddlewareSpy.calls.mostRecent().returnValue;
expect(httpCreateServerSpy).toHaveBeenCalledWith(middleware);
});
it('should log the server address info on \'listening\'', () => {
const server = createPreviewServer();
server.address = () => ({address: 'foo', family: '', port: 1337});
expect(loggerInfoSpy).not.toHaveBeenCalled();
server.emit('listening');
expect(loggerInfoSpy).toHaveBeenCalledWith('Up and running (and listening on foo:1337)...');
});
});
// Protected methods
describe('createBuildCreator()', () => {
let buildCreator: BuildCreator;
beforeEach(() => {
const api = new GithubApi(defaultConfig.githubToken);
const prs = new GithubPullRequests(api, defaultConfig.githubOrg, defaultConfig.githubRepo);
buildCreator = PreviewServerFactory.createBuildCreator(prs, defaultConfig.buildsDir, defaultConfig.domainName);
});
it('should pass the \'buildsDir\' to the BuildCreator', () => {
expect((buildCreator as any).buildsDir).toBe('builds/dir');
});
describe('on \'build.created\'', () => {
let prsAddCommentSpy: jasmine.Spy;
beforeEach(() => prsAddCommentSpy = spyOn(GithubPullRequests.prototype, 'addComment'));
it('should post a comment on GitHub for public previews', () => {
const commentBody = 'You can preview 1234567890 at https://pr42-1234567890.domain.name/.';
buildCreator.emit(CreatedBuildEvent.type, {pr: 42, sha: '1234567890', isPublic: true});
expect(prsAddCommentSpy).toHaveBeenCalledWith(42, commentBody);
});
it('should not post a comment on GitHub for non-public previews', () => {
buildCreator.emit(CreatedBuildEvent.type, {pr: 42, sha: '1234567890', isPublic: false});
expect(prsAddCommentSpy).not.toHaveBeenCalled();
});
});
describe('on \'pr.changedVisibility\'', () => {
let prsAddCommentSpy: jasmine.Spy;
beforeEach(() => prsAddCommentSpy = spyOn(GithubPullRequests.prototype, 'addComment'));
it('should post a comment on GitHub (for all SHAs) for PRs made public', () => {
const commentBody = 'You can preview 12345 at https://pr42-12345.domain.name/.\n' +
'You can preview 67890 at https://pr42-67890.domain.name/.';
buildCreator.emit(ChangedPrVisibilityEvent.type, {pr: 42, shas: ['12345', '67890'], isPublic: true});
expect(prsAddCommentSpy).toHaveBeenCalledWith(42, commentBody);
});
it('should not post a comment on GitHub if no SHAs were affected', () => {
buildCreator.emit(ChangedPrVisibilityEvent.type, {pr: 42, shas: [], isPublic: true});
expect(prsAddCommentSpy).not.toHaveBeenCalled();
});
it('should not post a comment on GitHub for PRs made non-public', () => {
buildCreator.emit(ChangedPrVisibilityEvent.type, {pr: 42, shas: ['12345', '67890'], isPublic: false});
expect(prsAddCommentSpy).not.toHaveBeenCalled();
});
});
it('should pass the correct parameters to GithubPullRequests', () => {
const prsAddCommentSpy = spyOn(GithubPullRequests.prototype, 'addComment');
buildCreator.emit(CreatedBuildEvent.type, {pr: 42, sha: '1234567890', isPublic: true});
buildCreator.emit(ChangedPrVisibilityEvent.type, {pr: 42, shas: ['12345', '67890'], isPublic: true});
const allCalls = prsAddCommentSpy.calls.all();
const prs = allCalls[0].object as GithubPullRequests;
expect(prsAddCommentSpy).toHaveBeenCalledTimes(2);
expect(prs).toBe(allCalls[1].object as GithubPullRequests);
expect(prs).toBeInstanceOf(GithubPullRequests);
expect(prs.repoSlug).toBe('organisation/repo');
});
});
describe('createMiddleware()', () => {
let buildRetriever: BuildRetriever;
let buildVerifier: BuildVerifier;
let buildCreator: BuildCreator;
let agent: supertest.SuperTest<supertest.Test>;
beforeEach(() => {
const circleCiApi = new CircleCiApi(defaultConfig.githubOrg, defaultConfig.githubRepo,
defaultConfig.circleCiToken);
const githubApi = new GithubApi(defaultConfig.githubToken);
const prs = new GithubPullRequests(githubApi, defaultConfig.githubOrg, defaultConfig.githubRepo);
const teams = new GithubTeams(githubApi, defaultConfig.githubOrg);
buildRetriever = new BuildRetriever(circleCiApi, defaultConfig.downloadSizeLimit, defaultConfig.downloadsDir);
buildVerifier = new BuildVerifier(prs, teams, defaultConfig.githubTeamSlugs, defaultConfig.trustedPrLabel);
buildCreator = new BuildCreator(defaultConfig.buildsDir);
const middleware = PreviewServerFactory.createMiddleware(buildRetriever, buildVerifier, buildCreator,
defaultConfig);
agent = supertest.agent(middleware);
});
describe('GET /health-check', () => {
it('should respond with 200', async () => {
await Promise.all([
agent.get('/health-check').expect(200),
agent.get('/health-check/').expect(200),
]);
});
it('should respond with 404 for non-GET requests', async () => {
await Promise.all([
agent.put('/health-check').expect(404),
agent.post('/health-check').expect(404),
agent.patch('/health-check').expect(404),
agent.delete('/health-check').expect(404),
]);
});
it('should respond with 404 if the path does not match exactly', async () => {
await Promise.all([
agent.get('/health-check/foo').expect(404),
agent.get('/health-check-foo').expect(404),
agent.get('/health-checknfoo').expect(404),
agent.get('/foo/health-check').expect(404),
agent.get('/foo-health-check').expect(404),
agent.get('/foonhealth-check').expect(404),
]);
});
});
describe('GET /can-have-public-preview/<pr>', () => {
const baseUrl = '/can-have-public-preview';
const pr = 777;
const url = `${baseUrl}/${pr}`;
let bvGetPrIsTrustedSpy: jasmine.Spy;
let bvGetSignificantFilesChangedSpy: jasmine.Spy;
beforeEach(() => {
bvGetPrIsTrustedSpy = spyOn(buildVerifier, 'getPrIsTrusted').and.resolveTo(true);
bvGetSignificantFilesChangedSpy = spyOn(buildVerifier, 'getSignificantFilesChanged').and.resolveTo(true);
});
it('should respond with 404 for non-GET requests', async () => {
await Promise.all([
agent.put(url).expect(404),
agent.post(url).expect(404),
agent.patch(url).expect(404),
agent.delete(url).expect(404),
]);
});
it('should respond with 404 if the path does not match exactly', async () => {
await Promise.all([
agent.get('/can-have-public-preview/42/foo').expect(404),
agent.get('/can-have-public-preview-foo/42').expect(404),
agent.get('/can-have-public-previewnfoo/42').expect(404),
agent.get('/foo/can-have-public-preview/42').expect(404),
agent.get('/foo-can-have-public-preview/42').expect(404),
agent.get('/fooncan-have-public-preview/42').expect(404),
]);
});
it('should respond appropriately if the PR did not touch any significant files', async () => {
bvGetSignificantFilesChangedSpy.and.resolveTo(false);
const expectedResponse = {canHavePublicPreview: false, reason: 'No significant files touched.'};
const expectedLog = `PR:${pr} - Cannot have a public preview, because it did not touch any significant files.`;
await agent.get(url).expect(200, expectedResponse);
expect(bvGetSignificantFilesChangedSpy).toHaveBeenCalledWith(pr, jasmine.any(RegExp));
expect(bvGetPrIsTrustedSpy).not.toHaveBeenCalled();
expect(loggerLogSpy).toHaveBeenCalledWith(expectedLog);
});
it('should respond appropriately if the PR is not automatically verifiable as "trusted"', async () => {
bvGetPrIsTrustedSpy.and.resolveTo(false);
const expectedResponse = {canHavePublicPreview: false, reason: 'Not automatically verifiable as "trusted".'};
const expectedLog =
`PR:${pr} - Cannot have a public preview, because not automatically verifiable as "trusted".`;
await agent.get(url).expect(200, expectedResponse);
expect(bvGetSignificantFilesChangedSpy).toHaveBeenCalledWith(pr, jasmine.any(RegExp));
expect(bvGetPrIsTrustedSpy).toHaveBeenCalledWith(pr);
expect(loggerLogSpy).toHaveBeenCalledWith(expectedLog);
});
it('should respond appropriately if the PR can have a preview', async () => {
const expectedResponse = {canHavePublicPreview: true, reason: null};
const expectedLog = `PR:${pr} - Can have a public preview.`;
await agent.get(url).expect(200, expectedResponse);
expect(bvGetSignificantFilesChangedSpy).toHaveBeenCalledWith(pr, jasmine.any(RegExp));
expect(bvGetPrIsTrustedSpy).toHaveBeenCalledWith(pr);
expect(loggerLogSpy).toHaveBeenCalledWith(expectedLog);
});
it('should respond with error if `getSignificantFilesChanged()` fails', async () => {
bvGetSignificantFilesChangedSpy.and.rejectWith('getSignificantFilesChanged error');
await agent.get(url).expect(500, 'getSignificantFilesChanged error');
expect(loggerErrorSpy).toHaveBeenCalledWith('Previewability check error', 'getSignificantFilesChanged error');
});
it('should respond with error if `getPrIsTrusted()` fails', async () => {
bvGetPrIsTrustedSpy.and.throwError('getPrIsTrusted error');
await agent.get(url).expect(500, 'getPrIsTrusted error');
expect(loggerErrorSpy).toHaveBeenCalledWith('Previewability check error', new Error('getPrIsTrusted error'));
});
});
describe('POST /circle-build', () => {
let getGithubInfoSpy: jasmine.Spy;
let getSignificantFilesChangedSpy: jasmine.Spy;
let downloadBuildArtifactSpy: jasmine.Spy;
let getPrIsTrustedSpy: jasmine.Spy;
let createBuildSpy: jasmine.Spy;
let IS_PUBLIC: boolean;
let BUILD_INFO: GithubInfo;
let AFFECTS_SIGNIFICANT_FILES: boolean;
let BASIC_PAYLOAD: CircleCiWebHookPayload;
const URL = '/circle-build';
const BUILD_NUM = 12345;
const PR = 777;
const SHA = 'COMMIT';
const DOWNLOADED_ARTIFACT_PATH = 'downloads/777-COMMIT-build.zip';
beforeEach(() => {
IS_PUBLIC = true;
BUILD_INFO = {
org: defaultConfig.githubOrg,
pr: PR,
repo: defaultConfig.githubRepo,
sha: SHA,
};
BASIC_PAYLOAD = { payload: { build_num: BUILD_NUM, build_parameters: { CIRCLE_JOB: 'aio_preview' } } };
AFFECTS_SIGNIFICANT_FILES = true;
getGithubInfoSpy = spyOn(buildRetriever, 'getGithubInfo')
.and.callFake(() => Promise.resolve(BUILD_INFO));
getSignificantFilesChangedSpy = spyOn(buildVerifier, 'getSignificantFilesChanged')
.and.callFake(() => Promise.resolve(AFFECTS_SIGNIFICANT_FILES));
downloadBuildArtifactSpy = spyOn(buildRetriever, 'downloadBuildArtifact')
.and.callFake(() => Promise.resolve(DOWNLOADED_ARTIFACT_PATH));
getPrIsTrustedSpy = spyOn(buildVerifier, 'getPrIsTrusted')
.and.callFake(() => Promise.resolve(IS_PUBLIC));
createBuildSpy = spyOn(buildCreator, 'create');
});
it('should respond with 400 if the request body is not in the correct format', async () => {
await Promise.all([
agent.post(URL).expect(400),
agent.post(URL).send().expect(400),
agent.post(URL).send({}).expect(400),
agent.post(URL).send({ payload: {} }).expect(400),
agent.post(URL).send({ payload: { build_num: -1 } }).expect(400),
agent.post(URL).send({ payload: { build_num: 4000 } }).expect(400),
agent.post(URL).send({ payload: { build_num: 4000, build_parameters: { } } }).expect(400),
agent.post(URL).send({ payload: { build_num: 4000, build_parameters: { CIRCLE_JOB: '' } } }).expect(400),
]);
});
it('should create a preview if everything is good and the build succeeded', async () => {
await agent.post(URL).send(BASIC_PAYLOAD).expect(201);
expect(getGithubInfoSpy).toHaveBeenCalledWith(BUILD_NUM);
expect(getSignificantFilesChangedSpy).toHaveBeenCalledWith(PR, jasmine.any(RegExp));
expect(downloadBuildArtifactSpy).toHaveBeenCalledWith(BUILD_NUM, PR, SHA, defaultConfig.buildArtifactPath);
expect(getPrIsTrustedSpy).toHaveBeenCalledWith(PR);
expect(createBuildSpy).toHaveBeenCalledWith(PR, SHA, DOWNLOADED_ARTIFACT_PATH, IS_PUBLIC);
});
it('should respond with 204 if the reported build is not the "AIO preview" job', async () => {
BASIC_PAYLOAD.payload.build_parameters.CIRCLE_JOB = 'lint';
await agent.post(URL).send(BASIC_PAYLOAD).expect(204);
expect(getGithubInfoSpy).not.toHaveBeenCalled();
expect(getSignificantFilesChangedSpy).not.toHaveBeenCalled();
expect(loggerLogSpy).toHaveBeenCalledWith(
'Build:12345, Job:lint -', 'Skipping preview processing because this is not the "aio_preview" job.');
expect(downloadBuildArtifactSpy).not.toHaveBeenCalled();
expect(getPrIsTrustedSpy).not.toHaveBeenCalled();
expect(createBuildSpy).not.toHaveBeenCalled();
});
it('should respond with 204 if the build did not affect any significant files', async () => {
AFFECTS_SIGNIFICANT_FILES = false;
await agent.post(URL).send(BASIC_PAYLOAD).expect(204);
expect(getGithubInfoSpy).toHaveBeenCalledWith(BUILD_NUM);
expect(getSignificantFilesChangedSpy).toHaveBeenCalledWith(PR, jasmine.any(RegExp));
expect(loggerLogSpy).toHaveBeenCalledWith(
'PR:777, Build:12345 - Skipping preview processing because this PR did not touch any significant files.');
expect(downloadBuildArtifactSpy).not.toHaveBeenCalled();
expect(getPrIsTrustedSpy).not.toHaveBeenCalled();
expect(createBuildSpy).not.toHaveBeenCalled();
});
it('should respond with 201 if the build is trusted', async () => {
IS_PUBLIC = true;
await agent.post(URL).send(BASIC_PAYLOAD).expect(201);
});
it('should respond with 202 if the build is not trusted', async () => {
IS_PUBLIC = false;
await agent.post(URL).send(BASIC_PAYLOAD).expect(202);
});
it('should fail if the CircleCI request fails', async () => {
// Note it is important to put the `reject` into `and.callFake`;
// If you just `and.returnValue` the rejected promise
// then you get an "unhandled rejection" message in the console.
getGithubInfoSpy.and.rejectWith('Test Error');
await agent.post(URL).send(BASIC_PAYLOAD).expect(500, 'Test Error');
expect(getGithubInfoSpy).toHaveBeenCalledWith(BUILD_NUM);
expect(downloadBuildArtifactSpy).not.toHaveBeenCalled();
expect(getPrIsTrustedSpy).not.toHaveBeenCalled();
expect(createBuildSpy).not.toHaveBeenCalled();
});
it('should fail if the Github organisation of the build does not match the configured organisation', async () => {
BUILD_INFO.org = 'bad';
await agent.post(URL).send(BASIC_PAYLOAD)
.expect(500, `Invalid webhook: expected "githubOrg" property to equal "organisation" but got "bad".`);
});
it('should fail if the Github repo of the build does not match the configured repo', async () => {
BUILD_INFO.repo = 'bad';
await agent.post(URL).send(BASIC_PAYLOAD)
.expect(500, `Invalid webhook: expected "githubRepo" property to equal "repo" but got "bad".`);
});
it('should fail if the artifact fetch request fails', async () => {
downloadBuildArtifactSpy.and.rejectWith('Test Error');
await agent.post(URL).send(BASIC_PAYLOAD).expect(500, 'Test Error');
expect(getGithubInfoSpy).toHaveBeenCalledWith(BUILD_NUM);
expect(downloadBuildArtifactSpy).toHaveBeenCalled();
expect(getPrIsTrustedSpy).not.toHaveBeenCalled();
expect(createBuildSpy).not.toHaveBeenCalled();
});
it('should fail if verifying the PR fails', async () => {
getPrIsTrustedSpy.and.rejectWith('Test Error');
await agent.post(URL).send(BASIC_PAYLOAD).expect(500, 'Test Error');
expect(getGithubInfoSpy).toHaveBeenCalledWith(BUILD_NUM);
expect(downloadBuildArtifactSpy).toHaveBeenCalled();
expect(getPrIsTrustedSpy).toHaveBeenCalled();
expect(createBuildSpy).not.toHaveBeenCalled();
});
it('should fail if creating the preview build fails', async () => {
createBuildSpy.and.rejectWith('Test Error');
await agent.post(URL).send(BASIC_PAYLOAD).expect(500, 'Test Error');
expect(getGithubInfoSpy).toHaveBeenCalledWith(BUILD_NUM);
expect(downloadBuildArtifactSpy).toHaveBeenCalled();
expect(getPrIsTrustedSpy).toHaveBeenCalled();
expect(createBuildSpy).toHaveBeenCalled();
});
});
describe('POST /pr-updated', () => {
const pr = '9';
const url = '/pr-updated';
let bvGetPrIsTrustedSpy: jasmine.Spy;
let bcUpdatePrVisibilitySpy: jasmine.Spy;
// Helpers
const createRequest = (num: number, action?: string) =>
agent.post(url).send({number: num, action});
beforeEach(() => {
bvGetPrIsTrustedSpy = spyOn(buildVerifier, 'getPrIsTrusted');
bcUpdatePrVisibilitySpy = spyOn(buildCreator, 'updatePrVisibility');
});
it('should respond with 404 for non-POST requests', async () => {
await Promise.all([
agent.get(url).expect(404),
agent.put(url).expect(404),
agent.patch(url).expect(404),
agent.delete(url).expect(404),
]);
});
it('should respond with 400 for requests without a payload', async () => {
const responseBody = `Missing or empty 'number' field in request: POST ${url} {}`;
const request1 = agent.post(url);
const request2 = agent.post(url).send();
await Promise.all([
request1.expect(400, responseBody),
request2.expect(400, responseBody),
]);
});
it('should respond with 400 for requests without a \'number\' field', async () => {
const responseBodyPrefix = `Missing or empty 'number' field in request: POST ${url}`;
const request1 = agent.post(url).send({});
const request2 = agent.post(url).send({number: null});
await Promise.all([
request1.expect(400, `${responseBodyPrefix} {}`),
request2.expect(400, `${responseBodyPrefix} {"number":null}`),
]);
});
it('should call \'BuildVerifier#gtPrIsTrusted()\' with the correct arguments', async () => {
await createRequest(+pr);
expect(bvGetPrIsTrustedSpy).toHaveBeenCalledWith(9);
});
it('should propagate errors from BuildVerifier', async () => {
bvGetPrIsTrustedSpy.and.rejectWith('Test');
await createRequest(+pr).expect(500, 'Test');
expect(bvGetPrIsTrustedSpy).toHaveBeenCalledWith(9);
expect(bcUpdatePrVisibilitySpy).not.toHaveBeenCalled();
});
it('should call \'BuildCreator#updatePrVisibility()\' with the correct arguments', async () => {
bvGetPrIsTrustedSpy.
withArgs(24).and.resolveTo(false).
withArgs(42).and.resolveTo(true);
await createRequest(24);
expect(bcUpdatePrVisibilitySpy).toHaveBeenCalledWith(24, false);
await createRequest(42);
expect(bcUpdatePrVisibilitySpy).toHaveBeenCalledWith(42, true);
});
it('should propagate errors from BuildCreator', async () => {
bcUpdatePrVisibilitySpy.and.rejectWith('Test');
await createRequest(+pr).expect(500, 'Test');
});
describe('on success', () => {
it('should respond with 200 (action: undefined)', async () => {
bvGetPrIsTrustedSpy.
withArgs(2).and.resolveTo(false).
withArgs(4).and.resolveTo(true);
const reqs = [4, 2].map(num => createRequest(num).expect(200, http.STATUS_CODES[200]));
await Promise.all(reqs);
});
it('should respond with 200 (action: labeled)', async () => {
bvGetPrIsTrustedSpy.
withArgs(2).and.resolveTo(false).
withArgs(4).and.resolveTo(true);
const reqs = [4, 2].map(num => createRequest(num, 'labeled').expect(200, http.STATUS_CODES[200]));
await Promise.all(reqs);
});
it('should respond with 200 (action: unlabeled)', async () => {
bvGetPrIsTrustedSpy.
withArgs(2).and.resolveTo(false).
withArgs(4).and.resolveTo(true);
const reqs = [4, 2].map(num => createRequest(num, 'unlabeled').expect(200, http.STATUS_CODES[200]));
await Promise.all(reqs);
});
it('should respond with 200 (and do nothing) if \'action\' implies no visibility change', async () => {
const promises = ['foo', 'notlabeled'].
map(action => createRequest(+pr, action).expect(200, http.STATUS_CODES[200]));
await Promise.all(promises);
expect(bvGetPrIsTrustedSpy).not.toHaveBeenCalled();
expect(bcUpdatePrVisibilitySpy).not.toHaveBeenCalled();
});
});
});
describe('ALL *', () => {
it('should respond with 404', async () => {
const responseFor = (method: string) => `Unknown resource in request: ${method.toUpperCase()} /some/url`;
await Promise.all([
agent.get('/some/url').expect(404, responseFor('get')),
agent.put('/some/url').expect(404, responseFor('put')),
agent.post('/some/url').expect(404, responseFor('post')),
agent.patch('/some/url').expect(404, responseFor('patch')),
agent.delete('/some/url').expect(404, responseFor('delete')),
]);
});
});
});
});
@@ -1,51 +0,0 @@
import * as express from 'express';
import {PreviewServerError} from '../../lib/preview-server/preview-error';
import {respondWithError, throwRequestError} from '../../lib/preview-server/utils';
describe('preview-server/utils', () => {
describe('respondWithError', () => {
let endSpy: jasmine.Spy;
let statusSpy: jasmine.Spy;
let response: express.Response;
beforeEach(() => {
endSpy = jasmine.createSpy('end');
statusSpy = jasmine.createSpy('status').and.callFake(() => response);
response = {status: statusSpy, end: endSpy} as any;
});
it('should set the status on the response', () => {
respondWithError(response, new PreviewServerError(505, 'TEST MESSAGE'));
expect(statusSpy).toHaveBeenCalledWith(505);
expect(endSpy).toHaveBeenCalledWith('TEST MESSAGE', jasmine.any(Function));
});
it('should convert non-PreviewServerError errors to 500 PreviewServerErrors', () => {
respondWithError(response, new Error('OTHER MESSAGE'));
expect(statusSpy).toHaveBeenCalledWith(500);
expect(endSpy).toHaveBeenCalledWith('OTHER MESSAGE', jasmine.any(Function));
});
});
describe('throwRequestError', () => {
it('should throw a suitable error', () => {
let caught = false;
try {
const request = {
body: 'The request body',
method: 'POST',
originalUrl: 'some.domain.com/path',
} as express.Request;
throwRequestError(505, 'ERROR MESSAGE', request);
} catch (error: any) {
caught = true;
expect(error).toBeInstanceOf(PreviewServerError);
expect(error.status).toEqual(505);
expect(error.message)
.toEqual(`ERROR MESSAGE in request: POST some.domain.com/path "The request body"`);
}
expect(caught).toEqual(true);
});
});
});
@@ -1,63 +0,0 @@
{
"compilerOptions": {
/* Basic Options */
"target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', or 'ESNEXT'. */
"module": "commonjs", /* Specify module code generation: 'none', commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
"lib": [
"es2015",
"es2016.array.include"
], /* Specify library files to be included in the compilation: */
// "allowJs": true, /* Allow javascript files to be compiled. */
// "checkJs": true, /* Report errors in .js files. */
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
// "declaration": true, /* Generates corresponding '.d.ts' file. */
// "sourceMap": true, /* Generates corresponding '.map' file. */
// "outFile": "./", /* Concatenate and emit output to single file. */
"outDir": "dist", /* Redirect output structure to the directory. */
// "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
// "removeComments": true, /* Do not emit comments to output. */
// "noEmit": true, /* Do not emit outputs. */
"importHelpers": true, /* Import emit helpers from 'tslib'. */
// "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
// "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
/* Strict Type-Checking Options */
"strict": true, /* Enable all strict type-checking options. */
// "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
// "strictNullChecks": true, /* Enable strict null checks. */
// "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
// "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
/* Additional Checks */
"noUnusedLocals": true, /* Report errors on unused locals. */
"noUnusedParameters": true, /* Report errors on unused parameters. */
"noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
/* Module Resolution Options */
// "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
// "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
// "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
"typeRoots": [
"node_modules/@types"
], /* List of folders to include type definitions from. */
// "types": [], /* Type declaration files to be included in compilation. */
// "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
/* Source Map Options */
// "sourceRoot": "./", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
// "mapRoot": "./", /* Specify the location where debugger should locate map files instead of generated locations. */
"inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
// "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
/* Experimental Options */
// "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
/* Other */
"forceConsistentCasingInFileNames": true, /* Disallow inconsistently-cased references to the same file. */
"newLine": "LF", /* Use the specified end of line sequence to be used when emitting files: "crlf" (windows) or "lf" (unix). */
"pretty": true, /* Stylize errors and messages using color and context. */
"skipLibCheck": true /* Skip type checking of all declaration files (*.d.ts). */
},
"include": [
"lib/**/*",
"test/**/*"
]
}
@@ -1,24 +0,0 @@
{
"defaultSeverity": "error",
"extends": [
"tslint:recommended"
],
"jsRules": {},
"rules": {
"array-type": [true, "array"],
"arrow-parens": [true, "ban-single-arg-parens"],
"interface-name": [true, "never-prefix"],
"max-classes-per-file": [true, 4],
"no-consecutive-blank-lines": [true, 2],
"no-console": [false],
"no-focused-test": true,
"no-namespace": [true, "allow-declarations"],
"no-skipped-test": true,
"no-string-literal": false,
"quotemark": [true, "single"],
"variable-name": [true, "ban-keywords", "check-format", "allow-leading-underscore"]
},
"rulesDirectory": [
"node_modules/tslint-jasmine-noSkipOrFocus"
]
}
File diff suppressed because it is too large Load Diff
@@ -1,9 +0,0 @@
#!/bin/bash
set -eu -o pipefail
# Set up env variables
export AIO_CIRCLE_CI_TOKEN=UNUSED_CIRCLE_CI_TOKEN
export AIO_GITHUB_TOKEN=$(head -c -1 /aio-secrets/GITHUB_TOKEN 2>/dev/null)
# Run the clean-up
node $AIO_SCRIPTS_JS_DIR/dist/lib/clean-up >> /var/log/aio/clean-up.log 2>&1
@@ -1,12 +0,0 @@
# Link the scripts on the host to the scripts in the container
# - the host scripts are mounted as a volume at `/dockerbuild`)
# - the original scripts are moved to `..._prod` in case they are needed later
# See `aio/aio-builds-setup/docs/misc--debug-docker-container.md` for more info
mv $AIO_SCRIPTS_SH_DIR ${AIO_SCRIPTS_SH_DIR}_prod
ln -s /dockerbuild/scripts-sh $AIO_SCRIPTS_SH_DIR
chmod a+x $AIO_SCRIPTS_SH_DIR/*
mv $AIO_SCRIPTS_JS_DIR ${AIO_SCRIPTS_JS_DIR}_prod
ln -s /dockerbuild/scripts-js $AIO_SCRIPTS_JS_DIR
@@ -1,86 +0,0 @@
#!/bin/bash
# Using `+e` so that all checks are run and we get a complete report (even if some checks failed).
set +e -u -o pipefail
# Variables
exitCode=0
# Helpers
function checkCert {
local certPath=$1
if [[ ! -f "$certPath" ]]; then
echo "Certificate '$certPath' does not exist. Skipping expiration check..."
return
fi
openssl x509 -checkend 0 -in "$certPath" -noout > /dev/null
reportStatus "Certificate '$certPath'"
if [[ $? -ne 0 ]]; then
echo " [WARN]"
echo " If you did not provide the certificate explicitly, try running the"
echo " 'docker build' command again with the '--no-cache' option to generate"
echo " a new self-signed certificate."
fi
}
function reportStatus {
local lastExitCode=$?
echo "$1: $([[ $lastExitCode -eq 0 ]] && echo OK || echo NOT OK)"
[[ $lastExitCode -eq 0 ]] || exitCode=1
return $lastExitCode
}
# Check services
services=(
rsyslog
cron
nginx
pm2-root
)
for s in ${services[@]}; do
service $s status > /dev/null
reportStatus "Service '$s'"
done
# Check SSL/TLS certificates expiration
certs=(
"$AIO_LOCALCERTS_DIR/$AIO_DOMAIN_NAME.crt"
"$TEST_AIO_LOCALCERTS_DIR/$TEST_AIO_DOMAIN_NAME.crt"
)
for c in ${certs[@]}; do
checkCert $c
done
# Check servers
origins=(
http://$AIO_PREVIEW_SERVER_HOSTNAME:$AIO_PREVIEW_SERVER_PORT
http://$AIO_NGINX_HOSTNAME:$AIO_NGINX_PORT_HTTP
https://$AIO_NGINX_HOSTNAME:$AIO_NGINX_PORT_HTTPS
)
for o in ${origins[@]}; do
curl --fail --silent $o/health-check > /dev/null
reportStatus "Server '$o'"
done
# Check resolution of external URLs
origins=(
https://google.com
)
for o in ${origins[@]}; do
curl --fail --silent $o > /dev/null
reportStatus "External URL '$o'"
done
# Exit
exit $exitCode
@@ -1,18 +0,0 @@
#!/bin/bash
set -eu -o pipefail
exec >> /var/log/aio/init.log
exec 2>&1
# Start the services
echo [`date`] - Starting services...
mkdir -p $AIO_NGINX_LOGS_DIR
mkdir -p $TEST_AIO_NGINX_LOGS_DIR
service rsyslog start
service cron start
service dnsmasq start
service nginx start
service pm2-root start
aio-preview-server-prod start
echo [`date`] - Services started successfully.
@@ -1,14 +0,0 @@
#!/bin/bash
set -eu -o pipefail
# Set up env variables for production
export AIO_CIRCLE_CI_TOKEN=$(head -c -1 /aio-secrets/CIRCLE_CI_TOKEN 2>/dev/null || echo "MISSING_CIRCLE_CI_TOKEN")
export AIO_GITHUB_TOKEN=$(head -c -1 /aio-secrets/GITHUB_TOKEN 2>/dev/null || echo "MISSING_GITHUB_TOKEN")
# Start the preview-server instance
action=$([ "$1" == "stop" ] && echo "stop" || echo "start")
pm2 $action $AIO_SCRIPTS_JS_DIR/dist/lib/preview-server \
--uid $AIO_WWW_USER \
--log /var/log/aio/preview-server-prod.log \
--name aio-preview-server-prod \
${@:2}
@@ -1,16 +0,0 @@
#!/bin/bash
set -eu -o pipefail
# Start the preview-server instance
appName=aio-preview-server-test
if [[ "$1" == "stop" ]]; then
pm2 delete $appName
else
source aio-test-env
pm2 start $AIO_SCRIPTS_JS_DIR/dist/lib/verify-setup/start-test-preview-server.js \
--uid $AIO_WWW_USER \
--log /var/log/aio/preview-server-test.log \
--name $appName \
--no-autorestart \
${@:2}
fi
@@ -1,19 +0,0 @@
# Set up env variables for testing
export AIO_NGINX_HOSTNAME=$TEST_AIO_NGINX_HOSTNAME
export AIO_NGINX_PORT_HTTP=$TEST_AIO_NGINX_PORT_HTTP
export AIO_NGINX_PORT_HTTPS=$TEST_AIO_NGINX_PORT_HTTPS
export AIO_ARTIFACT_PATH=$TEST_AIO_ARTIFACT_PATH
export AIO_BUILDS_DIR=$TEST_AIO_BUILDS_DIR
export AIO_DOMAIN_NAME=$TEST_AIO_DOMAIN_NAME
export AIO_GITHUB_ORGANIZATION=$TEST_AIO_GITHUB_ORGANIZATION
export AIO_GITHUB_REPO=$TEST_AIO_GITHUB_REPO
export AIO_GITHUB_TEAM_SLUGS=$TEST_AIO_GITHUB_TEAM_SLUGS
export AIO_SIGNIFICANT_FILES_PATTERN=$TEST_AIO_SIGNIFICANT_FILES_PATTERN
export AIO_TRUSTED_PR_LABEL=$TEST_AIO_TRUSTED_PR_LABEL
export AIO_PREVIEW_SERVER_HOSTNAME=$TEST_AIO_PREVIEW_SERVER_HOSTNAME
export AIO_PREVIEW_SERVER_PORT=$TEST_AIO_PREVIEW_SERVER_PORT
export AIO_ARTIFACT_MAX_SIZE=$TEST_AIO_ARTIFACT_MAX_SIZE
export AIO_CIRCLE_CI_TOKEN=TEST_CIRCLE_CI_TOKEN
export AIO_GITHUB_TOKEN=TEST_GITHUB_TOKEN
@@ -1,2 +0,0 @@
aio-verify-setup
ls -t /var/log/aio/preview-server-verify* | head -1 | xargs cat
@@ -1,44 +0,0 @@
#!/bin/bash
set -eu -o pipefail
logFile=/var/log/aio/verify-setup.log
previewServerLogFile=/var/log/aio/preview-server-verify-setup.log
exec 3>&1
exec >> $logFile
exec 2>&1
echo "[`date`] - Starting verification..."
# Helpers
function countdown {
message=$1
secs=$2
while [ $secs -gt 0 ]; do
echo -ne "$message in $secs...\033[0K\r"
sleep 1
: $((secs--))
done
echo -ne "\033[0K\r"
}
function onExit {
echo -e "Stopping Test Server"
aio-preview-server-test stop
echo -e "Full logs in '$logFile'.\n" > /dev/fd/3
}
# Setup EXIT trap
trap 'onExit' EXIT
# Start an preview-server instance for testing
echo -e "Starting Test Server"
aio-preview-server-test start --log $previewServerLogFile
# Give the preview-server some time to start :(
countdown "Starting" 5 > /dev/fd/3
# Run the tests
echo Running the tests
source aio-test-env
node $AIO_SCRIPTS_JS_DIR/dist/lib/verify-setup | tee /dev/fd/3

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