Compare commits

...

379 Commits

Author SHA1 Message Date
George Kalpakas
2fa61e8ee1 build(docs-infra): use pinned dependencies when possible in ng-packages-installer (#28510)
Previously, `ng-packages-installer` would replace the version ranges for
all dependencies that were peer dependencies of an Angular package with
the version range used in the Angular package. This effectively meant
that the pinned version (from `yarn.lock`) for that dependency was
ignored (even if the pinned version satisfied the new version range).

This commit reduces non-determinism in CI jobs using the locally built
Angular packages by always using pinned versions of dependencies for
Angular package peer dependencies if possible.

For example, assuming the following versions for the RxJS dependency:

- **aio/package.json**: `rxjs: ^6.3.0`
- **aio/yarn.lock**: `rxjs@^6.3.0: 6.3.3`
- **@angular/core#peerDependencies**: `rxjs: ^6.0.0`

...the following versions would be used with `ng-packages-installer`:

- Before this commit:
  - **aio/package.json**: `rxjs: ^6.0.0`
  - **node_modules/rxjs/**: `6.4.0` (latest version satisfying `^6.0.0`)
- After this commit:
  - **aio/package.json**: `rxjs: ^6.3.0`
  - **node_modules/rxjs/**: `6.3.3` (because it satisfies `^6.0.0`)

PR Close #28510
2019-03-06 19:14:13 +02:00
George Kalpakas
1eb0bc8ba8 build(docs-infra): keep other dependencies pinned when installing local Angular packages (#28510)
`ng-packages-installer` can be used to replace Angular packages with
locally built ones (from `dist/packages-dist/`) along with their peer
dependencies.

Previously, in order to achieve this, `yarn install` was called with the
`--no-lockfile` option, which resulted in installing the latest versions
of all dependencies (including transitive ones) permitted by the
corresponding version ranges in `package.json` files. As a result, newly
released versions would be picked, resulting in unexpected,
non-deterministic breakages in CI.

This commit calls `yarn install` with the `--pure-lockfile` option
instead. As a result, only the Angular packages (for which the locally
built ones are used) and their peer dependencies are unpinned; the
pinned versions from `yarn.lock` are used for all other (direct and
transitive) dependencies.

While this does not eliminate non-determinism across builds, it
significantly reduces it.

PR Close #28510
2019-03-06 19:05:25 +02:00
George Kalpakas
c2cde43b59 perf(docs-infra): avoid unnecessary I/O operation in ng-packages-installer (#28510)
PR Close #28510
2019-03-06 19:05:05 +02:00
George Kalpakas
bd4a3a88ae refactor(docs-infra): format package.json for readability in ng-packages-installer (#28510)
PR Close #28510
2019-03-06 19:04:35 +02:00
George Kalpakas
6ee866cb77 ci(docs-infra): fix deployment to Firebase
This is a backport of f1a860f to 5.2.x.
Related to #29030.
2019-03-06 18:55:31 +02:00
George Kalpakas
d8b63d0b83 build(compiler-cli): upgrade chokidar to latest version
This is a backport of 745c9c5 to 5.2.x.
Related to #28771.
2019-02-28 18:29:19 +02:00
Pete Bacon Darwin
a065dc24cd fix(compiler): remove bogus lines
These seem to have been added by mistake in
408d8cb735085bec4dcfab17bcf17b66368b98f2
2018-08-31 09:52:13 -07:00
Pete Bacon Darwin
45d90b58b9 ci: add docs-infra as a commit message scope 2018-08-31 09:52:13 -07:00
Pete Bacon Darwin
21a9f59ff2 docs(docs-infra): the build.sh script was renamed to create-image.sh 2018-08-31 09:52:13 -07:00
Pete Bacon Darwin
1c34e02ae6 ci(docs-infra): rename 'upload-server' to 'preview-server'
The server no longer has files uploaded to it. Instead it is more
accurate to refer to it as dealing with "previews" of PRs.
2018-08-31 09:52:13 -07:00
Pete Bacon Darwin
2f791ce68b ci(docs-infra): add explicit return types to methods 2018-08-31 09:52:13 -07:00
Pete Bacon Darwin
e5018c4d77 ci(docs-infra): improve preview-server logging 2018-08-31 09:52:13 -07:00
Pete Bacon Darwin
f7b041c7f5 ci(docs-infra): change AIO preview server stuff to pull builds from CircleCI
Previously, Travis pushed the build artitfacts to the preview server.
This required us to use JWT to secure the POST request from Travis, to
ensure we couldn't receive malicious builds.

JWT has been deprecated and we are moving our builds to CircleCI.

This commit rewrites the TypeScript part of the preview server that
handles converting build artifact into hosted previews of the docs.
2018-08-31 09:52:13 -07:00
Pete Bacon Darwin
08c8d7fe56 ci(docs-infra): factor out the aio-builds-setup environment variables 2018-08-31 09:52:13 -07:00
Pete Bacon Darwin
98e3ef4003 ci(docs-infra): move the payload-size check to the test job 2018-08-31 09:52:13 -07:00
Pete Bacon Darwin
ec3ecfcdf1 ci(docs-infra): add helper scripts for running TDD in Docker 2018-08-31 09:52:13 -07:00
Pete Bacon Darwin
32f67d9161 docs(docs-infra): update the preview server documentation
# Conflicts:
#	aio/aio-builds-setup/docs/vm-setup--start-docker-container.md
2018-08-31 09:52:13 -07:00
Pete Bacon Darwin
55c7430856 ci(docs-infra): move AIO preview deployment to CircleCI
Now instead of pushing the AIO build artifacts to the preview server
from inside a Travis job, the artifacts are built and hosted on the
CircleCI infrastructure. The preview server will then pull these
down after being triggered by a CircleCI build webhook.

# Conflicts:
#	.circleci/config.yml
2018-08-31 09:52:13 -07:00
Pete Bacon Darwin
2a276fcabe ci(docs-infra): update upload-server to run on node.js v10 2018-08-31 09:52:13 -07:00
Alex Rickabaugh
408d8cb735 build: release script should release v5-lts 2018-05-16 15:00:55 -07:00
Alex Rickabaugh
95e9107899 docs: add changelog for 5.2.11 2018-05-16 14:45:13 -07:00
Alex Rickabaugh
ad369903f1 release: cut the 5.2.11 release 2018-05-16 14:40:58 -07:00
George Kalpakas
bc27d4aae4 fix(service-worker): correctly handle requests with empty clientId (#23625)
Requests from clients that are not assigned a client ID by the browser
will produce `fetch` events with `null` or empty (`''`) `clientId`s.

Previously, the ServiceWorker only handled `null` values correctly. Yet
empty strings are also valid (see for example [here][1] and [there][2]).
With this commit, the SW will interpret _all_ falsy `clientId` values
the same (i.e. "no client ID assigned") and handle them appropriately.

Related Chromium issue/discussion: [#832105][3]

[1]: 4cc72bd0f1/docs/index.bs (L1392)
[2]: https://w3c.github.io/ServiceWorker/#fetchevent-interface
[3]: https://bugs.chromium.org/p/chromium/issues/detail?id=832105

Fixes #23526

PR Close #23625
2018-05-15 12:11:51 -07:00
George Kalpakas
3df879fe17 test(service-worker): support mock requests with null/empty client ID (#23625)
PR Close #23625
2018-05-15 12:11:51 -07:00
George Kalpakas
b004be5169 test(service-worker): improve adding clients in SwTestHarness (#23625)
This commits changes how clients are added in `SwTestHarness`, so that
the behavior in tests closer mimics what would happen in an actual
ServiceWorker.
It also removes auto-adding clients when calling `clients.get()`, which
could hide bugs related to non-existing clients.

PR Close #23625
2018-05-15 12:11:51 -07:00
Igor Minar
65dba9d0a8 test: fix firebase deployment script test
When I fixed the project id in 2c4850dc582287b7c34d4d26066fe4993638cbf0,
I didn't realize we had a test that verified the wrong behavior.
2018-05-04 15:09:54 -07:00
Igor Minar
354910203e fix(aio): correct project id for deployment of archive sites 2018-05-03 15:08:09 -07:00
Igor Minar
402f452761 docs(aio): add front page campaign for the ng-conf live stream (#23391)
PR Close #23391
2018-04-17 14:15:20 -07:00
Igor Minar
d2e7c99a93 docs: release notes for the 5.2.10 release 2018-04-16 01:21:13 -06:00
Igor Minar
80b9c65667 release: cut the 5.2.10 release 2018-04-16 01:19:21 -06:00
Tiep Phan
c78ae83b5a docs: update lifecycle hooks section in cheatsheet (#23320)
PR Close #23320
2018-04-15 23:44:44 -07:00
systemallica
509d440bce docs(aio): add missing word in the Component metadata section (#23384)
PR Close #23384
2018-04-15 23:36:56 -07:00
HiveSolution
7b23983859 fix(service-worker): add badge to NOTIFICATION_OPTION_NAMES (#23241)
Add badge to NOTIFICATION_OPTION_NAMES to support custom notification badge/icon.
Fixes #23196
PR Close #23241
2018-04-15 23:23:38 -07:00
Rafael
21f3301746 refactor: ensure all 'TODO's are consistent (#23252)
PR Close #23252
2018-04-13 13:12:00 -07:00
George Kalpakas
a3204f87fd test(aio): fix DocViewerComponent tests (#23359)
Obsolete assertions left over from #23249.

PR Close #23359
2018-04-13 08:13:34 -07:00
George Kalpakas
75d1ab9065 refactor(aio): remove file that should not be tracked (#23359)
PR Close #23359
2018-04-13 08:06:19 -07:00
Amit Portnoy
61bddebe65 fix(aio): remove additional 'googlebot' reference (#23249)
according to https://developers.google.com/search/reference/robots_meta_tag
googlebot is only used as a google specific override of 'robots'- there's no need for override in this case

PR Close #23249
2018-04-13 00:35:29 -07:00
Suguru Inatomi
31d95c2cd1 docs(aio): Add link to Japanese localization (#20630)
PR Close #20630
2018-04-13 00:26:13 -07:00
Camilo Giraldo
a1133303d3 refactor(language-service): fix typo on type.ts language-service 2018-04-13 00:08:54 -07:00
Juan Camilo Giraldo Chaverra
c1251a8430 docs(aio): fix typo on AOT compiler section 2018-04-13 00:07:36 -07:00
Jack Twilley
3cf5719435 docs: lock version of in-memory API (#23242)
The in-memory API has been updated for v6 but the Angular CLI has not.

Closes angular/in-memory-web-api#189
Fixes #22977
Fixes #23205

PR Close #23242
2018-04-13 00:01:19 -07:00
Michael Huang
47229fa87b docs(aio): update text InMemoryWebApiModule to HttpClientInMemoryWebApiModule (#23285)
PR Close #23285
2018-04-12 23:17:19 -07:00
Jeremy Attali
dabc076267 docs: fix typo in injected variable name (#23315)
The service injected is `ValueService`, however the name of the variable
does not reflect that. It's actually confusing since it's the name of
the `class` being created.

PR Close #23315
2018-04-12 23:16:53 -07:00
Nathan Friend
af3b308e63 docs(upgrade): fix detail regarding bootstrapping order (#23225) (#23270)
Clarify that Angular should be bootstrapped before AngularJS.

Closes angular/angular#23225

PR Close #23270
2018-04-12 23:16:18 -07:00
Jack Twilley
4ea8b17896 docs: lock version of in-memory API (#23242)
The in-memory API has been updated for v6 but the Angular CLI has not.

Closes angular/in-memory-web-api#189
Fixes #22977
Fixes #23205

PR Close #23242
2018-04-12 22:36:57 -07:00
Igor Minar
c5b6e31d97 build: fix aio size tracking, we need to use node_modules local to aio (#23328)
This fixes an issue introduced by 4f0cae067656fa4563417542297c67030d911a36 which removed firebase from the root node_modules.

PR Close #23328
2018-04-11 23:14:23 -07:00
Igor Minar
22686f8a2f build(aio): fix scripts/test-production.sh file permission issue
it needs to be executable for CI tests to run.
2018-04-10 18:32:56 -07:00
paulmojicatech
bda6908484 docs(core): update directives documentation (#23255)
fix(release): wrong input names in bank-account component

Directive example has errors #22382

PR Close #23255
2018-04-09 15:19:11 -07:00
Anuj Jain
a1231bed9c docs: fixed live example for the lifecycle hooks. (#23201)
PR Close #23201
2018-04-05 16:29:38 -07:00
George Kalpakas
a50ce6568a refactor(aio): remove unused images (#23018)
PR Close #23018
2018-04-05 10:12:10 -07:00
George Kalpakas
baa444ba7d docs(aio): update live-example docs in authors style guide (#23018)
PR Close #23018
2018-04-05 10:12:10 -07:00
George Kalpakas
73172dd67a ci(aio): upload the preview before checking the bundle sizes (#23123)
This makes the preview available even if the bundle sizes are out of
limits.

PR Close #23123
2018-04-05 10:11:00 -07:00
George Kalpakas
8aa49ac6d7 fix(aio): update trusted GitHub teams (angular-core --> team) (#23181)
PR Close #23181
2018-04-05 10:07:14 -07:00
Pawel Kozlowski
3900c36b1c refactor(common): simplify NgClass code, add comments (#21937)
PR Close #21937
2018-04-04 09:41:18 -07:00
Pawel Kozlowski
54e910841e fix(common): properly take className changes into account (#21937)
Fixes #21932

PR Close #21937
2018-04-04 09:41:18 -07:00
Steven Liekens
9703079e0b test(router): fix typo in expectation (#23137)
PR Close #23137
2018-04-04 08:22:05 -07:00
Dirk Luijk
dd615950d5 fix(forms): improve error message for invalid value accessors (#22731)
Signed-off-by: Dirk Luijk <mail@dirkluijk.nl>

PR Close #22731
2018-04-04 08:20:56 -07:00
Jason Bedard
ae76eeca6a fix(upgrade): propagate return value of resumeBootstrap (#22754)
Fixes #22723

PR Close #22754
2018-04-02 14:20:59 -07:00
Shannon Dunn
f43fba64cc fix(upgrade): correctly handle downgraded OnPush components (#22209)
Fixes #14286

PR Close #22209
2018-04-02 14:12:46 -07:00
George Kalpakas
4473da7de7 ci(aio): add monitoring for angular.io (#23093)
This commit configures a periodic job to be run on CircleCI, performing several
checks against the actual apps deployed to production (https://angular.io) and
staging (https://next.angular.io).

Fixes #21942

PR Close #23093
2018-03-30 15:27:21 -07:00
George Kalpakas
55eaeb17d9 fix(aio): fix SW routing RegExp to allow redirecting /api/animate URLs (#23093)
PR Close #23093
2018-03-30 15:27:21 -07:00
George Kalpakas
0614b2b941 refactor(aio): move deployment config tests and helpers around (#23093)
This commit prepares the ground for adding different types of tests.

PR Close #23093
2018-03-30 15:27:21 -07:00
George Kalpakas
650c6e56ec fix(aio): wait for the app to stabilize before registering the SW (#23093)
This commit also waits for the app to stabilize, before starting to
check for ServiceWorker updates. This avoids setting up a long timeout,
which would prevent the app from stabilizing and thus cause issues with
Protractor.

PR Close #23093
2018-03-30 15:27:21 -07:00
Victor Berchet
4f7c369847 fix(compiler): fix support for html-like text in translatable attributes (#23053)
PR Close #23053
2018-03-29 08:58:29 -07:00
Teamop
e7b2e97b46 style(aio): fix typo in the scrollbar (#23064)
PR Close #23064
2018-03-29 08:57:43 -07:00
George Kalpakas
0d4fe38a09 fix(service-worker): ignore invalid only-if-cached requests (#22883)
Under some circumstances (possibly related to opening Chrome DevTools),
requests are made with `cache: 'only-if-cached'` and `mode: 'no-cors'`.
These request will eventually fail, because `only-if-cached` is only
allowed to be used with `mode: 'same-origin'`.
This is likely a bug in Chrome DevTools.

This commit avoids errors related to such requests by not handling them.

Fixes #22362

PR Close #22883
2018-03-28 10:02:18 -07:00
George Kalpakas
ae9c25ff3d fix(service-worker): do not enter degraded mode when offline (#22883)
Previously, when trying to fetch `ngsw.json` (e.g. during
`checkForUpdate()`) while either the client or the server were offline,
the ServiceWorker would enter a degrade mode, where only existing
clients would be served. This essentially meant that the ServiceWorker
didn't work offline.
This commit fixes it by differentiating offline errors and not entering
degraded mode. The ServiceWorker will remain in the current mode until
connectivity to the server is restored.

Fixes #21636

PR Close #22883
2018-03-28 10:02:18 -07:00
George Kalpakas
d0f575bc54 test(service-worker): minor test fixes and refactorings (#22883)
PR Close #22883
2018-03-28 10:02:18 -07:00
Joey Perrott
776bb8206f build: update browserstack key (#23026)
PR Close #23026
2018-03-27 14:56:13 -04:00
George Kalpakas
d7f4aa6936 docs(common): add HttpParamsOptions to the public API (#20332)
Fixes #20276

PR Close #20332
2018-03-23 16:31:11 -04:00
George Kalpakas
4be8b3f481 docs: update available platforms for test.sh (#22958)
PR Close #22958
2018-03-23 14:01:46 -04:00
Alberto Piras
8d1e64004b docs(aio): fix TS warning error - filter expects a boolean function param (#22954)
PR Close #22954
2018-03-23 13:07:58 -04:00
Alex Eagle
49f6d1d02e build: rm --noimplicit_deps from bazel query (#22912)
I added this option for demos, so that it would be easier to see a graphviz graph of the dependency structure without all the node_modules edges.

However, `ibazel` picks up this option as well, and means it doesn't trigger on changes that only appear through an implicit dependency.
PR Close #22912
2018-03-22 18:03:39 -04:00
Matias Niemelä
641cc493ff fix(animations): avoid animation insertions during router back/refresh (#21977)
Closes #19712

PR Close #21977
2018-03-22 17:59:41 -04:00
Jinhyuk Kim
a846abbb95 docs: fix a typo in aot compiler guide (#22876)
PR Close #22876
2018-03-21 13:20:51 -07:00
Andrew Seguin
50761fb73e test: remove gulp public-api:update docs (#22914)
PR Close #22914
2018-03-21 13:15:23 -07:00
adasilva
65f8943aab fix(service-worker): fix LruList bugs (#22769)
'remove' method not removing url from state.map
'accessed' method not removing 'previous' reference from existing  node when it becomes the head

Fixes #22218
Fixes #22768

PR Close #22769
2018-03-21 13:11:27 -07:00
Domas Trijonis
5391f96406 fix(upgrade): two-way binding and listening for event (#22772)
Changes would not propagate to a value in downgraded component in case you had two-way binding and listening to a value-change, e.g. [(value)]="value" (value-change)="fetch()"

Closes #22734

PR Close #22772
2018-03-19 22:44:36 -05:00
Igor Minar
aca4735c8b ci: update yarn.lock (#22857) 2018-03-19 06:42:31 -07:00
Igor Minar
9ee2e9e032 ci: improve logging when running aio/examples e2e tests (#22854)
PR Close #22854
2018-03-18 13:57:00 -07:00
Igor Minar
2fe7595235 build: update to zone.js@0.8.20 (#22854)
PR Close #22854
2018-03-18 13:57:00 -07:00
Igor Minar
1dd7cebad1 build: remove obsolete rollup-test (#22854)
PR Close #22854
2018-03-18 13:57:00 -07:00
Judy Bogart
2731ecafbf docs: incorporate suggestions and corrections from gkalpak (#21569)
PR Close #21569
2018-03-15 14:48:35 -07:00
Judy Bogart
e1b82a0a64 docs(aio): update architecture section (#21569)
PR Close #21569
2018-03-15 14:48:35 -07:00
Igor Minar
bbd54285d8 build: update to tsickle@0.27.2 (#22789)
PR Close #22789
2018-03-15 11:41:43 -07:00
Victor Berchet
7d9de17935 build: add release helper scripts (#22378) (#22781)
PR Close #22378

PR Close #22781
2018-03-15 11:38:14 -07:00
Chuck Jazdzewski
db0afa9394 fix(compiler-cli): emit correct css string escape sequences (#22776)
Works around an issue with TypeScript 2.6 and 2.7 that causes
the tranformer emit to emit incorrect escapes for css string
literals.

Fixes: #22774

PR Close #22776
2018-03-15 11:37:51 -07:00
Kara Erickson
5298b2bda3 docs: add changelog for 5.2.9 2018-03-14 14:59:27 -07:00
Kara Erickson
25ae886cad release: cut the 5.2.9 release 2018-03-14 14:56:14 -07:00
Adam Plumer
fc6dfc2e08 fix(platform-server): add styles to elements correctly (#22527)
* Partially reverts #22263 due to lack of total spec compliance
  on the server
* Maintains the camel-case styles fix

PR Close #22527
2018-03-14 14:12:32 -07:00
Suguru Inatomi
c0670ef52d docs(aio): add ng-japan 2018 to events (#22750)
ng-japan 2018 will be held at June 16 in Tokyo, Japan! 

https://ngjapan.org/en.html
PR Close #22750
2018-03-14 10:59:57 -07:00
Pete Bacon Darwin
fe96cafd03 fix(aio): constrain error logging to improve reporting (#22713)
The `Logger.error()` method now only accepts a single `Error` parameter
and passes this through to the error handler.
This allows the error handler to serialize the error more accurately.

The various places that use `Logger.error()` have been updated.

See #21943#issuecomment-370230047

PR Close #22713
2018-03-14 10:52:12 -07:00
Ward Bell
ad674dad37 docs: testing - highlight dispatchEvent (#22726)
PR Close #22726
2018-03-14 10:21:42 -07:00
Jason Aden
86517f2ad5 fix(router): correct over-encoding of URL fragment (#22687)
Relates to: #10280 #22337

PR Close #22687
2018-03-11 22:15:02 -07:00
Kapunahele Wong
6d9a4f8aea docs: refactor revert() and call to lifecylce hook, edit doc to changes (#22094)
PR Close #22094
2018-03-08 10:58:43 -08:00
Alex Eagle
a1efc27ff2 ci: double our cores on CircleCI (#22641)
This should cut our build time in ~half, assuming it's widely parallel.
See
https://circleci.com/docs/2.0/configuration-reference/#resource_class

Also enable bazel repository caching, and store the external
repositories in the CircleCI cache for later builds.

PR Close #22641
2018-03-07 21:00:04 -08:00
Kapunahele Wong
311232004c docs: add HeroService to code tabs and fix headers (#22373)
PR Close #22373
2018-03-07 18:20:54 -08:00
Alex Eagle
2a236b4066 docs: add changelog for 5.2.8 2018-03-07 14:45:07 -08:00
Alex Eagle
bdee824292 release: cut the 5.2.8 release 2018-03-07 14:44:18 -08:00
Igor Minar
4aeb04dcb0 docs: update RELEASE_SCHEDULE.md by pushing out v6 rc by one week
We are pushing RC and Final out by one week because of RxJS v6 complications that are blocking the release. No further delays are currently expected.
2018-03-07 10:51:30 -08:00
yerkebulan
5876fb0125 docs(aio): update deprecated Http reference to HttpClientModule, remove Http reference because another context is used (#21984)
docs(aio): change HttpClientModule reference to HttpClient

docs(aio): capitalize Http to HTTP

docs(aio): fix typo mistake in 'universal' guide

docs(aio): gets rid of the parentheses and the "e.g." in 'universal' guide

PR Close #21984
2018-03-06 15:03:54 -08:00
Ward Bell
5b7b208637 docs: fix cli-quickstart doc and specs (#22338)
* tests were broken
* incorrect instructions.
* didn't match current CLI template for new project

PR Close #22338
2018-03-06 09:41:54 -08:00
Jason Aden
789a47ec44 fix(router): fix URL serialization so special characters are only encoded where needed (#22337)
This change brings Angular largely in line with how AngularJS previously serialized URLs. This is based on RFC 3986 and resolves issues such as the above #10280 where URLs could be parsed, re-serialized, then parsed again producing a different result on the second parsing.

Adjustments to be aware of in this commit:

* URI fragments will now serialize the same as query strings
* In the URI path or segments (portion prior to query string and/or fragment), the plus sign (`+`) and ampersand (`&`) will appear decoded
* In the URL path or segments, parentheses values (`(` and `)`) will now appear percent encoded as `%28` and `%29` respectively
* In the URL path or segments, semicolons will be encoded in their percent encoding `%3B`

NOTE: Parentheses and semicolons denoting auxillary routes or matrix params will still appear in their decoded form -- only parentheses and semicolons used as values in a segment or key/value pair for matrix params will be encoded.

While these changes are not considered breaking because applications should be decoding URLs and key/value pairs, it is possible that some unit tests will break if comparing hard-coded URLs in tests since that hard coded string will represent the old encoding. Therefore we are releasing this fix in the upcoming Angular v6 rather than adding it to a patch for v5.

Fixes: #10280

PR Close #22337
2018-03-06 06:58:08 -08:00
SangKa.Z
984a13e45b docs(aio): fix table header (#22553)
PR Close #22553
2018-03-05 10:13:17 -08:00
Alex Eagle
a3f7e30153 build: update to latest bazel rules (#22558)
PR Close #22558
2018-03-02 13:28:01 -08:00
Alex Eagle
ff7e2e3f1e ci: speed up lint job on CircleCI (#22526)
When I enabled bazel remote caching, I also switched to running
buildifier and skylint from the package.json script, which builds them
from head. With remote caching, we do get cache hits for these, but
looking up the action inputs actually takes quite a bit of time since we
have to first fetch the remote repository, then do loading and
analysis, then read the inputs to determine the cache key.

It's more important to keep the lint job fast, so I'm reverting that
part of the change for now. We can experiment with building them from
head in a less critical repo.

PR Close #22526
2018-03-01 09:12:59 -08:00
Rado Kirov
fe0d53f3a9 build: Add support for bazelOptions.maxCacheSizeMb in ngc-wrapped. (#22511)
PR Close #22511
2018-03-01 08:41:11 -08:00
Pete Bacon Darwin
27962f8949 build(aio): improve accuracy of code auto-linking (#22494)
The new version of `dgeni-packages/typescript` no longer strips
out "namespaces" from types, which was part of the problem of
not autolinking correctly to `HttpEventType.Response`.

Another part of the problem was that we did not include `.`
characters when matching potential code blocks for auto-linking,
which precluded properties of enums from being linked.

Finally, members we not being given a `path` property, which is
needed to effectively autolink to them. This is now set in
the `simplifyMemberAnchors` processor.

Closes #21375

PR Close #22494
2018-03-01 08:12:25 -08:00
Pete Bacon Darwin
855e3a65db build(aio): move link disambiguation from getLinkInfo to getDocFromAlias (#22494)
The disambiguation needs to be done earlier so that the auto-link-code
post-processor can benefit from it.

PR Close #22494
2018-03-01 08:11:19 -08:00
Pete Bacon Darwin
f8e70fb0c6 build(aio): initialise exampleMap correctly (#22502)
The `exampleMap` needs to hold an hash object for each
of the `collectExamples.exampleFolders` paths.

Previously these hash objects were only created if there
was actually an example file the hash's respective
example folder.  This could cause crashes during
`yarn docs-watch` (and so also `yarn sync-and-serve`)
if no examples were read in for a particular run of
the doc-gen.

PR Close #22502
2018-03-01 08:10:15 -08:00
Alex Eagle
697d31a38c docs: add changelog for 5.2.7 2018-02-28 15:11:02 -08:00
Alex Eagle
1593bff1b0 release: cut the 5.2.7 release 2018-02-28 15:11:02 -08:00
Kapunahele Wong
0ec11e3223 docs: fix dynamic component loader example (#22181)
closes #21903

PR Close #22181
2018-02-28 10:46:38 -08:00
Alex Eagle
089769d5c3 build: update ts-api-guardian version (#22402)
PR Close #22402
2018-02-28 09:29:30 -08:00
Alan Agius
9137650dba build: update api golden files (#22402)
`ts-api-guardion` has been updated to accept new TypeScript syntax

PR Close #22402
2018-02-28 09:29:30 -08:00
Stephen Fluin
eccce1772d docs: fix community tab in GitHub by copying CoC 2018-02-27 19:04:00 -08:00
George Kalpakas
4aef9de37e fix(upgrade): correctly destroy nested downgraded component (#22400)
Previously, when a downgraded component was destroyed in a way that did
not trigger the `$destroy` event on the element (e.g. when a parent
element was removed from the DOM by Angular, not AngularJS), the
`ComponentRef` was not destroyed and unregistered.
This commit fixes it by listening for the `$destroy` event on both the
element and the scope.

Fixes #22392

PR Close #22400
2018-02-27 18:41:03 -08:00
Jason Aden
f2fa7a289f docs(aio): add Observable and Rx docs (#21423)
PR Close #21423
2018-02-27 11:24:31 -08:00
haoRchen
e1fbe20d98 style(aio): updated padding-right for the .alert class in _heading-anchors.scss (#22431)
The h3 element is overflowing over its surrounding div element. Modified padding-right to align consistently with the remainder of div contents.

fixes: #22407

PR Close #22431
2018-02-26 17:52:30 -08:00
haoRchen
38bd8d49a5 style(aio): added padding-left to h3 in _subsection.scss (#22431)
The h3 element is overflowing over its surrounding div element. Modified padding-left to align consistently with the remainder of div contents.

fixes: #22407

PR Close #22431
2018-02-26 17:52:30 -08:00
Olivier Combe
d033106adb docs: update i18n guide for projects that don't use the cli (#21767)
PR Close #21767
2018-02-26 17:51:58 -08:00
Adam Plumer
de02a7a5de fix(platform-server): generate correct stylings for camel case names (#22263)
* Add correct mapping from camel case to kebab case for CSS style
names
* Remove internal CSS methods in favor of native Domino APIs

Fixes #19235

PR Close #22263
2018-02-26 17:46:21 -08:00
Ward Bell
c30a942329 docs: testing guide for CLI (#20697)
- updates tests
- heavy prose revisions
- uses HttpClient (with angular-in-memory-web-api)
- test HeroService using `HttpClientTestingModule`
- scrub away most By.CSS
- fake async observable with `asyncData()`
- extensive Twain work
- different take on retryWhen
- remove app barrels (& systemjs.extras) which troubled plunker/systemjs
- add dummy export const to hero.ts (plunkr/systemjs fails w/o it)
- shrink and re-organize TOC
- add marble testing package and tests
- demonstrate the "no beforeEach()" test coding style
- add section on Http service testing
- prepare for stackblitz
- confirm works in plunker except excluded marble test
- add tests for avoidFile class feature of CodeExampleComponent

PR Close #20697
2018-02-26 13:40:24 -08:00
Jamie Krug
2a38d93171 docs(aio): fix doc typo referring to httpOptions (#22456)
The variable name mention should match the actual tutorial code.

PR Close #22456
2018-02-26 13:32:44 -08:00
George Kalpakas
a9a0e27e94 fix(upgrade): fix empty transclusion content with AngularJS@>=1.5.8 (#22167)
The function provided by `ngUpgrade` as `parentBoundTranscludeFn` when
upgrading a component with transclusion, will break in AngularJS v1.5.8+
if no transclusion content is provided. The reason is that AngularJS
will try to destroy the transclusion scope (which would not be needed
any more). But since the transcluded content comes from Angular, not
AngularJS, there is no transclusion scope to destroy.
This commit fixes it by providing a dummy scope object with a no-op
`$destroy()` method.

Fixes #22175

PR Close #22167
2018-02-25 10:06:14 -08:00
George Kalpakas
66383901a6 fix(upgrade): correctly handle = bindings in @angular/upgrade (#22167)
Previously, having a `=` binding on an upgraded components would result
in setting the corresponding property to an EventEmitter function. This
should only happen for `&` bindings.
This commit rstrores the correct behavior.

Note:
The issue was only present in the dynamic version of `ngUpgrade`. The
static version worked as expected.
The error did not show up in tests, because in AngularJS v1.5.x a
function would be serialized to an empty string in interpolations, thus
making them indistinguishable from uninitialized properties (in the
view). The serialization behavior changed in AngularJS v1.6.x, making
the errors visible.

PR Close #22167
2018-02-25 10:06:14 -08:00
George Kalpakas
1eb54132e4 test(upgrade): run tests against multiple AngularJS versions (#22167)
Fixes #19332

PR Close #22167
2018-02-25 10:06:14 -08:00
George Kalpakas
6c9c173e1e refactor(upgrade): use correct paths for imports (#22167)
`packages/upgrade/static/src` is anymlink to `packages/upgrade/src`.
Still, using the correct paths (e.g. using
`@angular/upgrade/static/src/...` for `@angula/upgrade/static` specs
ensures that the module loader (e.g. SystemJS) can map the imports to
the same instances.

PR Close #22167
2018-02-25 10:06:14 -08:00
George Kalpakas
1e08a945e1 test(platform-browser): remove stray debugger statement (#22167)
PR Close #22167
2018-02-25 10:06:14 -08:00
Victor Berchet
4a08745d3e build: add support for the "merge-assistance" label in merge-pr (#22414)
fixes #22256

PR Close #22414
2018-02-23 12:58:30 -08:00
hariharanjv
cf91906d8f docs(aio): Essential JS 2 url updated (#19739)
PR Close #19739
2018-02-23 11:18:12 -08:00
ROOT\hariharanjv
0723c04a01 docs(aio): Essential JS 2 UI Components. (#19739)
PR Close #19739
2018-02-23 11:18:12 -08:00
Alex Eagle
2b7188906b build: fix 5.2.x merge (#22408)
PR Close #22408
2018-02-23 10:21:33 -08:00
Alex Eagle
17c1577de9 ci: don't use bazel git_repository rule (#22406)
It's currently broken on CircleCI because of a TLS change made by GitHub.
This is okay as a permanent change, we don't really want bazel to fetch a full git history.

Fixes #22405

PR Close #22406
2018-02-23 09:41:11 -08:00
Kapunahele Wong
150bac310f docs: fix deployment sample path (#22048)
PR Close #22048
2018-02-22 13:40:57 -08:00
Jason Aden
8f0a0641e2 fix(router): don't mutate route configs (#22358)
Fixes #22203

PR Close #22358
2018-02-22 13:35:38 -08:00
Alex Eagle
17762390c9 build: disable bazel-out symlink (#22375)
It causes headaches on MacOS High Sierra, see https://github.com/bazelbuild/bazel/issues/4603

PR Close #22375
2018-02-22 13:33:12 -08:00
Kapunahele Wong
da1b4d5ea7 docs: fix ngmodules-jsmodules pre-req (#22316)
closes #22157

PR Close #22316
2018-02-22 11:20:48 -08:00
Kapunahele Wong
aa100f69f2 docs: edit styleguide recommendation on components as elements (#22074)
Change recommendation on using attributes for components since there are use cases including the use of <button mat-button> in MD

Closes #19401.

PR Close #22074
2018-02-22 11:20:21 -08:00
Martin Probst
9cca5a8c9c build: allow passing node options to ngc. (#22245)
PR Close #22245
2018-02-22 10:23:54 -08:00
Victor Berchet
7c3b95b4ab
docs: add changelog for 5.2.6 2018-02-21 16:45:10 -08:00
Victor Berchet
e4e8a68c06
release: cut the 5.2.6 release 2018-02-21 16:45:09 -08:00
Victor Berchet
6460ac0add
Revert "feat(platform-browser): fix #19604, can config hammerOptions (#21979)"
This reverts commit fdbfd21bcdae775bb6fbaa6e407998f796fd2613.
2018-02-21 16:45:09 -08:00
Trotyl
ee91de9d5a feat(core): support metadata reflection for native class types (#22356)
closes #21731

PR Close #22356
2018-02-21 16:09:27 -08:00
Trotyl
5ec38f2f47 fix(core): properly handle function without prototype in reflector (#22284)
closes #19978

PR Close #22284
2018-02-21 14:52:05 -08:00
Marco Alvarado
612cfeca14 docs(aio): updates directive event hooks real capabilities (#16654)
Minor documentation update to include event hooks that were assumed to only work on components.

Closes angular/angular#10221

PR Close #16654
2018-02-21 14:51:05 -08:00
zhilich
dfdade25ea docs(aio): Wrong code example. Form status field was added later in the guide. (#21275)
PR Close #21275
2018-02-21 11:06:48 -08:00
Ricardo M
c2f78e1ca3 docs(http): fix a typo in code comment (#22327)
PR Close #22327
2018-02-21 11:06:06 -08:00
Victor Berchet
484802cd2a build: make git revert messages valid (#22339)
`git revert` default message is "Revert <original message>" (no semi-colon)

PR Close #22339
2018-02-21 11:05:35 -08:00
Kapunahele Wong
ee535777bb docs: add ngStyle to cheat sheet (#22070)
PR Close #22070
2018-02-20 16:08:16 -08:00
Ro Savage
94756eb4bd docs(aio): fix incorrect quote mark usage (#22335)
PR Close #22335
2018-02-20 15:42:55 -08:00
Oussama Ben Brahim
23b0707707 docs(aio): fix the css of the heroes component's buttons (#22333)
Fixes #22222

PR Close #22333
2018-02-20 15:41:57 -08:00
Pascal Zwick
a2cb0109f1 docs(aio): Fix name of component (#22332)
PR Close #22332
2018-02-20 15:41:35 -08:00
Stephen Fluin
d20a08bc48 docs(aio): update installed mobile tool list (#22331)
PR Close #22331
2018-02-20 15:41:15 -08:00
Alex Eagle
5bdb3acace build: update tsickle dep from compiler-cli (#22295)
PR Close #22295
2018-02-20 15:40:45 -08:00
Alex Eagle
c5418c7abe fix(compiler-cli): add missing entry point to package, update tsickle (#22295)
PR Close #22295
2018-02-20 15:40:45 -08:00
Jeff Cross
09b4612bdd docs(aio): add Nx and Angular Enterprise Playbook to resources (#22321)
PR Close #22321
2018-02-20 10:09:34 -08:00
Frederik Prijck
a346d28df6 test(language-service): fix minor typos (#21372)
PR Close #21372
2018-02-20 10:08:55 -08:00
Victor Berchet
bf07837d5d test(common): fix ngIf tests
The failing test was ported for the master branch which ignores whitespaces
2018-02-18 20:12:47 -08:00
Victor Berchet
cdfedc1e49 fix(common): fix merge error in ng_if.ts 2018-02-18 19:51:15 -08:00
Victor Berchet
af6a0563de fix(common): then and else template might be set to null (#22298)
PR Close #22298
2018-02-18 19:28:36 -08:00
Johannes Hoppe
c726d1d6d3 docs(aio): add angular-buch to resources (#22163)
adds a link to the website of our book. second version of the text. thanks!

PR Close #22163
2018-02-18 15:12:14 -08:00
Johannes Hoppe
2030846df7 docs(aio): add angular-buch to resources (#22163)
adds a link to the website of our book. many thanks for reviewing this

PR Close #22163
2018-02-18 15:12:14 -08:00
Derk Snijders - i3anaan
9dae97c5d9 docs: correct grammar mistakes in CONTRIBUTING.md (#22285)
Various grammar mistakes were present in the contribution guidelines
This commit corrects some of them

PR Close #22285
2018-02-18 13:27:24 -08:00
George Kalpakas
228eb9feef fix(aio): improve announcement-bar layout with wide logos (#22272)
PR Close #22272
2018-02-18 13:16:30 -08:00
Joe Eames
debf01d7a6 docs(aio): added ngconf announcement (#22272)
PR Close #22272
2018-02-18 13:16:30 -08:00
Victor Berchet
51abe69b60 fix: merge-pr script (#22290)
PR Close #22290
2018-02-18 13:13:29 -08:00
Victor Berchet
396bc0d9e9 build: use authenticated mode for the merge script (#22269)
`TOKEN` is the name with use for other GH scripts

PR Close #22269
2018-02-18 13:01:51 -08:00
Olivier Combe
861250b4e2 docs: fix changelog errors (#22228)
PR Close #22228
2018-02-16 18:03:04 -08:00
Fabian Wiles
81c1e0a3c3 docs: replace plnkr with StackBlitz (#20365)
PR Close #20365
2018-02-16 15:12:10 -08:00
Trotyl Yu
ce5e8fad9e fix(common): correct mapping of Observable methods (#20518)
fixes #20516
PR Close #20518
2018-02-16 15:10:31 -08:00
Alex Eagle
185a6ab562 build: add esm5 build (#22258)
This is a partial cherry-pick of 370ab66c4f020da7e1a6b25b96109f1b3cbf9cba
which included this along with a new feature for ivy.

PR Close #22258
2018-02-16 14:49:24 -08:00
George Kalpakas
6b457843b9 test(aio): increase docs-test timeouts to prevent flakes on Travis (#22261)
PR Close #22261
2018-02-16 14:46:23 -08:00
Alex Eagle
5f52ea3d06 feat(bazel): ng_module produces bundle index (#22176)
It creates the bundle index .d.ts and .metadata.json files.
The names are based on the ng_module target.

PR Close #22176
2018-02-15 14:17:16 -08:00
Alex Eagle
6c1e7ac40e feat(bazel): introduce a binary stamping feature (#22176)
This grabs version control metadata and makes it available in the build, eg. to put in the version field for released artifacts

PR Close #22176
2018-02-15 14:08:54 -08:00
Alex Eagle
6597616aac refactor(bazel): convert most ts_library to ng_module (#22176)
This is necessary so we can produce ng metadata for our packages that are published as libraries

PR Close #22176
2018-02-15 14:08:54 -08:00
Victor Berchet
6a57264d38 Revert: "build: allow bazel build ... (#22168)"
This reverts commit 3237f1dbfc0bfbd018627d2ffddf63cc01a69e95.
2018-02-15 14:07:41 -08:00
Stefanie Fluin
1e3e0fad49 fix(aio): improve printing styles (#19651)
printfix

PR Close #19651
2018-02-14 18:49:59 -05:00
Adrien Boullé
0c88d5dedd style: fix typos boostrap to bootstrap (#21917)
PR Close #21917
2018-02-14 18:21:52 -05:00
Matias Niemelä
0b8b06ee8b build: comment-out chromium version checking code temporarily (#22232)
Related #22231

PR Close #22232
2018-02-14 17:26:43 -05:00
Pat
edd6cd4e29 docs: typo - components should be possessive (#22172)
PR Close #22172
2018-02-14 15:06:52 -05:00
JiaLi.Passion
fdbfd21bcd feat(platform-browser): fix #19604, can config hammerOptions (#21979)
PR Close #21979
2018-02-14 15:02:59 -05:00
Kapunahele Wong
0a5283da1a docs(aio): fix extraneous divs (#22069)
PR Close #22069
2018-02-14 15:02:36 -05:00
Alex Eagle
3237f1dbfc build: allow bazel build ... (#22168)
Note, the reason this commit removes `firebase-tools` is:

1) firebase-tools has an optional dependency on
https://www.npmjs.com/package/@google-cloud/functions-emulator
2) yarn's `--ignore-optional` doesn't work for transitive deps, so
there's no way to yarn install without getting that functions-emulator
package
3) functions-emulator has a transitive dep on `grpc`
4) the version of `grpc` we get has `BUILD` files and no `WORKSPACE`
file so it always breaks `bazel build ...`

It could be solved by any of:
1) remove firebase-tools - this is what I did
2) fix yarn so you can omit optional deps of a transitive dep
3) make functions-emulator depend transitively on a more recent `grpc`
version
4) patch `grpc` after install by doing an `rm` command in our
postinstall or something

In its place we must install protobufjs. This is needed by the
ngc-wrapped test, which needs jasmine as well as bazel's worker mode
dependencies, and therefore cannot simply rely on
node_modules =
"@build_bazel_rules_typescript_tsc_wrapped_deps//:node_modules"

PR Close #22168
2018-02-14 15:01:42 -05:00
Misko Hevery
106b435297 docs: add changelog for 5.2.5 2018-02-13 21:07:53 -08:00
Misko Hevery
ca69dfde31 release: cut the 5.2.5 release 2018-02-13 21:06:54 -08:00
Trotyl
1b8ea1005a fix(compiler): make unary plus operator consistent to JavaScript (#22154)
fixes #22089

PR Close #22154
2018-02-13 13:04:30 -08:00
adasilva
da6ab91095 fix(platform-browser): support 0/false/null values in transfer_state (#22179)
Issue #22178

PR Close #22179
2018-02-13 11:28:22 -08:00
guillaumebarranco
c4f841f031 fix(core): add stacktrace in log when error during cleanup component in TestBed (#22162)
PR Close #22162
2018-02-13 11:28:08 -08:00
Olivier Combe
e576b69d10 ci: update ngbot config file (#22173)
Fixes #22053
PR Close #22173
2018-02-13 10:26:06 -08:00
Alex Eagle
f938774748 ci: remove conditional clause for bazel install (#22170)
No longer needed since we don't have a bazel job
PR Close #22170
2018-02-13 10:25:51 -08:00
Alex Eagle
c0b78067c4 ci: remove bazel job from Travis (#22170)
This saves us an executor on Travis.

Note that we still do a bazel build on travis when we run the integration tests under e2e_2.

We expect that CircleCI is the only place we'll ever consume bazel-built artifacts.

PR Close #22170
2018-02-13 10:25:51 -08:00
Alex Eagle
2931018b84 ci: enable bazel remote caching on CircleCI (#21784)
This should cause Bazel builds to be incremental, only re-building parts of Angular affected by changes since the last build.
It also fixes a potential version skew, where CI was running the Bazel linter binaries in the ngcontainer docker image, but developers built them using the versions in WORKSPACE

PR Close #21784
2018-02-13 10:14:22 -08:00
Oussama Ben Brahim
e97afae67a docs(aio): fix typo in "preserveWhitespaces" example (#22182)
Fixes #22147

PR Close #22182
2018-02-12 15:57:42 -08:00
Chuck Jazdzewski
c6bdc831c1 fix(common): weaken AsyncPipe transform signature (#22169)
The AsyncPipe type signature was changed to allow
deferred creation of promises and observalbes that
is supported by the implementation by allowing
`Promise<T>|null|undefined` and by allowing
`Observable<T>|null|undefined`.

PR Close #22169
2018-02-12 15:57:29 -08:00
mgmarlow
1a897e4f48 docs(aio): add angular-playground to resources (#22042)
PR Close #22042
2018-02-12 14:30:58 -08:00
Jason Aden
603e50d3bf refactor(router): move activation to private method (#22144)
PR Close #22144
2018-02-12 10:41:18 -08:00
Johannes Hoppe
ca5b72461c docs(aio): add angular.schule to resources (#22164)
adds a link to our website. many thanks for reviewing this

PR Close #22164
2018-02-12 10:01:23 -08:00
Suguru Inatomi
a616dd6c17 fix(aio): remove broken span closing tag (#22146)
PR Close #22146
2018-02-12 10:01:10 -08:00
yerkebulan
0ed64af5bc docs(aio): put structural directives back in the nav (#21856)
PR Close #21856
2018-02-12 10:00:14 -08:00
Alex Eagle
d57fd0b84f fix(bazel): allow TS to read ambient typings (#21876)
Same fix as e70d7a2a7c
This is because the CompilerOptions needs to have directoryExists undefined in order to get the google3 behavior,
so we have to set the property outside the constructor.

Fixes #21872

PR Close #21876
2018-02-09 17:16:25 -08:00
Miško Hevery
9b280eef63 build: merge-pr new checks that all requested changes have been addressed (#21817)
PR Close #21817
2018-02-09 17:14:17 -08:00
Pete Bacon Darwin
d4a9db2af5 fix(aio): do not rewrite /styleguide URL in Service Worker (#22104)
This URL needs to be redirected via the server, so
we must exclude it from being rewitten.

Closes #22078

PR Close #22104
2018-02-09 13:10:48 -08:00
Pete Bacon Darwin
2740b69023 docs(aio): update docs changelog with links to ts-to-js guide (#21763) (#22104)
PR Close #21763

PR Close #22104
2018-02-09 13:10:48 -08:00
Pete Bacon Darwin
15ff7ba880 fix(aio): update Firebase redirects and SW routes (#21763) (#22104)
Closes #21377

PR Close #21763

PR Close #22104
2018-02-09 13:10:48 -08:00
Pete Bacon Darwin
615bb95138 build(aio): test Service Worker "routing" configuration (#21763) (#22104)
PR Close #21763

PR Close #22104
2018-02-09 13:10:48 -08:00
Pete Bacon Darwin
2b67400652 build(aio): test Firebase hosting redirection configuration (#21763) (#22104)
PR Close #21763

PR Close #22104
2018-02-09 13:10:48 -08:00
Mikhail Burshteyn
ac815f7281 docs: fix typo in http.md (#22058)
PR Close #22058
2018-02-09 13:10:24 -08:00
Pete Bacon Darwin
854f0ff33a feat(aio): enable data driven homepage announcements (#22043)
PR Close #22043
2018-02-09 13:10:12 -08:00
Trotyl
c55523466f docs(aio): several fix for ngmodule guides (#21517)
PR Close #21517
2018-02-09 13:03:47 -08:00
Miško Hevery
f80b9da18a Revert "docs(common): add HttpParamsOptions to the public API (#20332)"
This reverts commit de561f36e10b244af8817775a2486cf2a1fc5314.
2018-02-08 14:38:13 -08:00
Alex Eagle
c5ec8d952a fix(bazel): improve error message for missing assets (#22096)
fixes #22095

PR Close #22096
2018-02-08 10:01:27 -08:00
George Kalpakas
de561f36e1 docs(common): add HttpParamsOptions to the public API (#20332)
Fixes #20276

PR Close #20332
2018-02-08 09:44:37 -08:00
Peter Bacon Darwin
47b71d98ae fix(core): use appropriate inert document strategy for Firefox & Safari (#17019)
Both Firefox and Safari are vulnerable to XSS if we use an inert document
created via `document.implementation.createHTMLDocument()`.

Now we check for those vulnerabilities and then use a DOMParser or XHR
strategy if needed.

Further the platform-server has its own library for parsing HTML, so we
sniff for that (by checking whether DOMParser exists) and fall back to
the standard strategy.

Thanks to @cure53 for the heads up on this issue.

PR Close #17019
2018-02-08 08:55:15 -08:00
Pete Bacon Darwin
22d548f4ed docs(aio): remove ngATL banner from homepage (#22060)
Closes #22029

PR Close #22060
2018-02-07 16:10:18 -08:00
Kapunahele Wong
799edd1e2f docs(aio): remove lifecycle hooks img (#21425)
PR Close #21425
2018-02-07 16:09:44 -08:00
Jesus Rodriguez
ffcb5dd264 build(aio): update examples to CLI to 1.6.5 (#21222)
PR Close #21222
2018-02-07 16:09:27 -08:00
Pete Bacon Darwin
75897d4977 feat(aio): report logger.error calls to Google Analytics (#22011)
We have a number of observables that have `catch` handlers to recover
from errors without causing the stream to close, and breaking the app.
We also have some `try ... catch` blocks for synchronous code for a
similar reason.

In these cases we conventionally then call `logger.error` in the catch
handler. We are interested in these errors so we are going to capture them
by reporting them to Google Analytics via the new `ReportingErrorHandler`.

PR Close #22011
2018-02-07 12:09:38 -08:00
Pete Bacon Darwin
eb0da530a7 feat(aio): report application errors to Google Analytics (#22011)
This is a basic implementation of error logging using the limited
facilities provided by Google Analytics.

Errors within the Angular app itself will be handled by a new
`ReportingErrorHandler` service, which overrides and extends the
built-in `ErrorHandler`.

Further, errors outside the app, which arrive at `window.onerror`
will also be reported to Google Analytics.

Closes #21943

PR Close #22011
2018-02-07 12:09:38 -08:00
Chuck Jazdzewski
0b23573573 fix(language-service): correct instructions to install the language service (#22000)
Fixes: #21956

PR Close #22000
2018-02-07 12:09:00 -08:00
Emilio
47b73fd153 fix(core): ensure initial value of QueryList length (#21980) (#21982)
Set initial value of `length` to `0`.

Fixes regression introduced by e544742156 (diff-a85dbe0991a7577ea24b49374e9ae90b) where the `length` property ceased to have initial value.

Closes #21980

PR Close #21982
2018-02-07 12:08:44 -08:00
Olivier Combe
07769e5caa test(common): disable deprecated date pipe tests on chrome mobile (#21933)
Closes #21907
PR Close #21933
2018-02-07 12:07:31 -08:00
Oussama Ben Brahim
9fdb804b63 test(forms): update test name with correct wording (#21833)
Use the term primitive value instead of standalone

Fixes #21831

PR Close #21833
2018-02-07 12:07:15 -08:00
George Kalpakas
9d02db3254 fix(aio): ignore .header-link when selecting the heading text (#21695)
Implemented @maxkorz's
[suggestion](https://github.com/angular/angular/issues/21515#issuecomment-357453634).

Fixes #21515

PR Close #21695
2018-02-07 12:06:45 -08:00
George Kalpakas
920b0df32a fix(aio): prevent heading misplacement while styles load (#21695)
During the initial load of the page (probably until the icon styles are
loaded and/or applied), the `.header-link` element is wider, pushing the
heading text slightly to the right (for a brief moment).

This commit prevents this slight shift by explicitly setting the width
for the `.header-link` element.

PR Close #21695
2018-02-07 12:06:45 -08:00
George Kalpakas
b313976ac1 fix(aio): ensure header-links are visible at <600px (#21695)
PR Close #21695
2018-02-07 12:06:45 -08:00
George Kalpakas
2d19e7bbea refactor(aio): simplify .header-link styles (#21695)
PR Close #21695
2018-02-07 12:06:45 -08:00
George Kalpakas
c3c92d7796 fix(aio): reduce flicker and reflows for initial rendering (#21695)
For the initial rendering, where there is no transition from a previous
visual state to a new one, animations make little sense. The page should
load with as few reflows as possible.
Similarly, while we typically want to defer updating the SideNav state
(e.g. opened/closed) until the "leaving" document is animated out of the
page, on the initial rendering (where there is no "leaving" document)
this leads to the SideNav flashing (from closed to open).

These worked as expected before, but several parts (mostly related to
documents with a SideNav) have been accidentally broken in recent
commits (e.g. when upgraded to latest material, or enabled animations
for DocViewer transitions, etc.).

This commit restores the previous behavior by ensuring that (on the
initial rendering) the SideNav state is updated as soon as possible and
that there will be no animations when:

1. The hamburger button appears.
2. The SideNav is opened.
3. The main section's width is adjusted to make room for the SideNav.

PR Close #21695
2018-02-07 12:06:45 -08:00
George Kalpakas
a887c9339f refactor(aio): preserve HttpClient asynchronicity in tests (#21695)
Previously, the mocked `HttpClient` was synchronous in tests (despite
the actual `HttpClient` being asynchronous). Although we use observables
(which generally make the implementation sync/async-agnostic), the fact
that we have no control over when Angular updates/checks views and calls
lifecycle hooks resulted in different behavior (and errors) in tests
(with sync `HttpClient`) vs actual app (with async `HttpClient`).

This commit ensures that the behavior (and errors) are consistent
between the tests and the actual app by making the mocked `HttpClient`
asynchronous.

PR Close #21695
2018-02-07 12:06:45 -08:00
George Kalpakas
89051a0452 fix(aio): remove links from sub-menu toggles (#21695)
Navigating to a document while trying to expand or collapse a sub-menu
is undesirable and confusing. All sub-menu toggles should have no other
effect than expanding/collapsing the corresponding sub-menu.

PR Close #21695
2018-02-07 12:06:45 -08:00
George Kalpakas
97dafa8460 docs(animations): fix typo (disbled --> disabled) (#21695)
PR Close #21695
2018-02-07 12:06:45 -08:00
Kevin Fahy
56b9591746 fix(forms): prevent event emission on enable/disable when emitEvent is false (#12366) (#21018)
Previously, the emitEvent flag was only checked when emitting on the current control.
Thus, if  the control was part of a hierarchy, events were emitted on the parent and the childrens.
This fixes the issue by properly passing the emitEvent flag to both parent and childrens.

Fixes #12366

PR Close #21018
2018-02-07 12:05:26 -08:00
Pete Bacon Darwin
b0ddb5ad0e build(aio): blacklist unwanted URLs from the generated sitemap.xml (#22061)
Closes #22017

PR Close #22061
2018-02-07 12:02:01 -08:00
jhenderson2099
103727aadf docs(aio): fix TOH inclusion of HeroesService. (#21228)
Change docs where the MessageService is referenced

Fixes #20398

PR Close #21228
2018-02-07 12:01:32 -08:00
Aric Thorn
44ea80b797 docs(forms): Custom Validator example selector name incorrect. (#20464)
Added bobby e2e test for template form.

Fixes: #20206

PR Close #20464
2018-02-07 12:01:12 -08:00
Aric Thorn
41046e4a90 docs(forms): Custom Validator example selector name incorrect. (#20464)
Name of selector in ForbiddenName example is not consistent with Validator class nor Html selector example. Added the selector name 'appForbiddenName' as an alias name for the input of the Validator class, and updated the view accordingly.

Fixes: #20206

PR Close #20464
2018-02-07 12:01:12 -08:00
Alex Rickabaugh
530b824faa docs: add changelog for 5.2.4 2018-02-07 10:19:39 -08:00
Alex Rickabaugh
e22d3a605c release: cut the 5.2.4 release 2018-02-07 10:16:42 -08:00
Martin Sikora
c6645e7a04 fix(core): fix proper propagation of subscriptions in EventEmitter (#22016)
Closes #21999

PR Close #22016
2018-02-06 07:56:34 -08:00
A7xSV
f0396f1e54 docs(aio): fix swap value (#20905)
'http.get' has been swapped in for 'of'

PR Close #20905
2018-02-05 13:05:58 -08:00
Judy Bogart
adb1d62967 docs: clarify npm/yarn commands, add blank lines to mix md/html in table (#21606)
PR Close #21606
2018-02-05 13:02:14 -08:00
Judy Bogart
cfe83939a4 docs: update browser support (#21606)
PR Close #21606
2018-02-05 13:02:14 -08:00
Miško Hevery
973607fe9d ci: mark PRs with rejection as not green (#21922)
PR Close #21922
2018-02-05 13:01:12 -08:00
Pete Bacon Darwin
664f7fa477 build(aio): add API static members to search index (#21988)
Previously searching for `compose` did not include `Validators`
in the search results because we were not including all the
`static` members of API docs in the index.

PR Close #21988
2018-02-05 13:00:47 -08:00
Olivier Combe
b155ae116b ci: add config for g3 status (#21996)
Ref #21642
PR Close #21996
2018-02-05 12:59:59 -08:00
JiaLi.Passion
ce51ea93a1 fix(core): fix #20582, don't need to wrap zone in location change listener (#22007)
PR Close #22007
2018-02-05 12:59:05 -08:00
Pete Bacon Darwin
d38e08812e feat(aio): dynamically, pre-emptively, add noindex (#21992)
These tags are removed when the doc is ready and valid, but this will
allow us to block indexing in the case that the Angular app fails to
bootstrap or load the document for some non-404 reason.

This should get around the problem with hardcoded tags. See
c3fb820473

Closes #21941

PR Close #21992
2018-02-05 12:58:27 -08:00
JiaLi.Passion
aa9ba7f9fe fix(core): should check Zone existance when scheduleMicroTask (#20656)
PR Close #20656
2018-02-02 07:53:55 -08:00
Jannis Lehmann
102d06b974 docs: consistency fix in describing a custom tag (#21747)
PR Close #21747
2018-02-02 07:53:18 -08:00
JiaLi.Passion
11ec80a053 docs: add docs for IE (#21824)
PR Close #21824
2018-02-02 07:51:47 -08:00
musicq
75eecdc351 docs: add missing underline (#21892)
PR Close #21892
2018-02-02 07:49:33 -08:00
Pete Bacon Darwin
965eecc587 build(aio): move zip and live-example generation to yarn predocs task (#21970)
This will prevent the confusing errors for first time users who
try to generate the docs with `yarn docs` and are told there are
dangling links.

Closes #21944

PR Close #21970
2018-02-02 07:48:42 -08:00
Alex Rickabaugh
c4fb696189 fix(common): don't convert null to a string when flushing a mock request (#21417)
A bug in TestRequest caused null response bodies to be stringified. This
change causes null to be treated faithfully.

Fixes #20744

PR Close #21417
2018-02-01 08:32:44 -08:00
yerkebulan
72df747dd6 docs(aio): add missing closing <code-examle> tag (#21771)
PR Close #21771
2018-02-01 08:31:21 -08:00
Jason Aden
579bed1a7a docs: add changelog for 5.2.3 2018-01-31 12:47:02 -08:00
Jason Aden
b59fb23f4a release: cut the 5.2.3 release 2018-01-31 12:45:17 -08:00
Ward Bell
2aa460b30e docs: add http guide sample and adjust text (#21326)
PR Close #21326
2018-01-31 10:24:43 -08:00
Enzo Volkmann
e0022ae9cd docs: Fix platform-detection example for Universal (#21796)
PR Close #21796
2018-01-31 10:21:04 -08:00
Pete Bacon Darwin
f2e923edd8 build(aio): upgrade to dgeni-packages 0.24.0 (#21802)
This has two benefits:

* it prepares the way for the API docs update, which need parameter docs
* it doesn't incorrectly report dangling links for non-latin anchors

Closes #21306

PR Close #21802
2018-01-31 10:20:37 -08:00
Olivier Combe
c2f5ed545c fix(common): generate closure-locale data file with exported plural functions (#21873)
Fixes #21870
PR Close #21873
2018-01-30 11:42:31 -08:00
Olivier Combe
5d75df8fb1 ci: unblock master by ignoring date pipe tests while we fix it (#21906)
PR Close #21906
2018-01-30 11:33:46 -08:00
Alex Rickabaugh
ed2b71799c fix(common): allow HttpInterceptors to inject HttpClient (#19809)
Previously, an interceptor attempting to inject HttpClient directly
would receive a circular dependency error, as HttpClient was
constructed via a factory which injected the interceptor instances.
Users want to inject HttpClient into interceptors to make supporting
requests (ex: to retrieve an authentication token). Currently this is
only possible by injecting the Injector and using it to resolve
HttpClient at request time.

Either HttpClient or the user has to deal specially with the circular
dependency. This change moves that responsibility into HttpClient
itself. By utilizing a new class HttpInterceptingHandler which lazily
loads the set of interceptors at request time, it's possible to inject
HttpClient directly into interceptors as construction of HttpClient no
longer requires the interceptor chain to be constructed.

Fixes #18224.

PR Close #19809
2018-01-29 16:12:32 -08:00
Kara Erickson
fad99cca0e fix(forms): inserting and removing controls should work in re-bound form arrays (#21822)
Closes #21501

PR Close #21822
2018-01-29 16:11:41 -08:00
Bernd Hacker
3f5ead3845 fix(aio): missing plural s in preserveWhiteSpaces example (#21854)
PR Close #21854
2018-01-29 11:35:13 -08:00
Eli Sadoff
a89e709515 docs: change ”it's" to "its" as needed in several docs. (#21867)
Most of them are in content but one is in common and needs special approval.

PR Close #21867
2018-01-29 11:34:47 -08:00
Alex Eagle
6a7689d4ea build: update to latest bazel rules (#21821)
PR Close #21821
2018-01-27 10:55:45 -08:00
Igor Minar
696ba01a4e fix(aio): don't set noindex metatag in the static index.html (#21816)
This seems to be causing crawling issues for google.

Ref #21665

PR Close #21816
2018-01-26 16:08:31 -08:00
Victor Berchet
81d64d6bec fix(core): fix retrieving the binding name when an expression changes (#21814)
fixes #21735
fixes #21788

PR Close #21814
2018-01-26 15:34:48 -08:00
Miško Hevery
7410941a7c build: merge-pr now checks that PR status is green before proceeding (#21810)
Optionally one can use `--force` to override and merge no non-green PR.

PR Close #21810
2018-01-26 14:50:41 -08:00
George Kalpakas
d159ad8b88 build(aio): prevent Windows error on serve-and-sync (#21806)
Running `yarn start` (which watches the `src/` directory) and
`yarn docs-watch` (which cleans up files in `src/generated/api/`) often
results in `ENOTEMPTY` errors.

This commit solves it by ensuring that `yarn docs` has been completed
before running `yarn start`.

PR Close #21806
2018-01-26 14:50:15 -08:00
Chuck Jazdzewski
250c8da768 fix(language-service): ensure correct paths are passed to TypeScript (#21812)
The 2.6 version of TypeScript's `resolveModuleName`  started to
require paths passed to be separated by '/' instead of being
able to handle '\'.

`ngc` and `ng` already do this transformation.

Fixes: #21811

PR Close #21812
2018-01-26 14:49:23 -08:00
Chuck Jazdzewski
778e6e759f fix(language-service): spell diagnostics correctly (#21812)
PR Close #21812
2018-01-26 14:49:23 -08:00
Patrick Shields
35a0721217 fix(router): remove @internal tag on ParamInheritanceType (#21773)
This is a more defensive approach to ensure that references to
ParamInheritanceType from the published declarations do not cause
compilation errors when compiling Angular from the published packages.

Fixes #21456

PR Close #21773
2018-01-26 10:28:34 -08:00
Judy Bogart
ba045e88d7 docs: add notes on email used for CLA (#21754)
Closes #20034

PR Close #21754
2018-01-26 10:28:18 -08:00
George Kalpakas
67806a7b25 fix(aio): close SideNav on non-sidenav doc on wide screen (#21538)
Partly addresses #21520.

PR Close #21538
2018-01-26 10:25:16 -08:00
George Kalpakas
9778a23be8 fix(aio): fix SideNav height on narrow screens (#21538)
Since we specify `bottom: 0`, specifying the height is unnecessary and
leads to wrong height (unless updated) on narrow screens where the
topbar height is decreased.

Partly addresses #21520.

PR Close #21538
2018-01-26 10:25:15 -08:00
Alex Eagle
87e06d765e ci: Add back the CLI integration test with pinning (#21555)
The CLI app is now checked in, rather than generated dynamically with
`ng new`. This loses some assertion power, but gains hermeticity.
It also checks in lock files for all integration tests, avoiding
floating version numbers.

We'll need another place to integration test between changes in
the various repositories - but the angular/angular PR-blocking status
is not the right place to do this.

PR Close #21555
2018-01-25 22:18:56 -08:00
Oussama Ben Brahim
56f3e18c1c fix(forms): allow FormBuilder to create controls with any formState type (#20917)
Align formState type in FormBuilder#control with FormControl#constructor

Fixes #20368

PR Close #20917
2018-01-25 22:17:43 -08:00
Miško Hevery
637515e71b build: autosquashes SHAs as part of merge-pr script (#21791)
To support `git checkin --fixup` and `git checkin —squash`
we need to make sure that `merge-pr` squashes the sepecial
commits before they are merged.

For more details see:
https://robots.thoughtbot.com/autosquashing-git-commits

PR Close #21791
2018-01-25 22:12:11 -08:00
Pete Bacon Darwin
27ecd077d4 docs(aio): fix missing stylesheet in component-styles example (#21772)
The code in the example was referring to `hero-app.component.css` but this did
not exist.

PR Close #21772
2018-01-25 13:38:14 -08:00
Pete Bacon Darwin
4db1be0292 docs(aio): fix paths to imported CSS stylesheets (#21772)
The AOT compiler needs relative paths so that it can find
the imported stylesheets.

PR Close #21772
2018-01-25 13:38:14 -08:00
Pete Bacon Darwin
a0dbef9ea4 build(aio): upgrade CLI version to cope with new Angular 6.0.0-beta.1 release (#21772)
Before version 1.6 of Angular CLI there was a check that prevented use of Angular
compiler CLI with major version 6.

PR Close #21772
2018-01-25 13:38:13 -08:00
Misko Hevery
3aaf43f73c docs: add changelog for 5.2.2 2018-01-24 21:26:17 -08:00
Misko Hevery
d952ae24dd release: cut the 5.2.2 release 2018-01-24 21:23:04 -08:00
Alex Eagle
da9e57b3d5 build: Update to latest rules_typescript. (#21675)
Fixes #21481

PR Close #21675
2018-01-24 20:47:41 -08:00
Max Ivanov
44d4f82dae docs(aio): added a link to Angular-RU (#21687)
Angular-RU Community on GitHub is a single point for all resources, chats, podcasts and meetups for Angular in Russia

PR Close #21687
2018-01-24 20:47:40 -08:00
George Kalpakas
bde2b4425c ci: use sudo: false on Travis (#21641)
Related to #21422.

PR Close #21641
2018-01-24 20:47:40 -08:00
George Kalpakas
2a3de802a0 fix(aio): fix code highlight in API docs templates (#21630)
Fixes #21108

PR Close #21630
2018-01-24 20:47:40 -08:00
Olivier Combe
71f9eaa743 fix(common): extract plural function from i18n locale data files for TS 2.6 (#21626)
Fixes #21608

PR Close #21626
2018-01-24 20:47:40 -08:00
Olivier Combe
a62c186d15 fix(common): don't remove special characters when extracting CLDR data (#21626)
PR Close #21626
2018-01-24 20:47:40 -08:00
Pete Bacon Darwin
c8bf281174 build(aio): generate sitemap from the generated pages (#21689)
Closes #21684

PR Close #21689
2018-01-24 20:47:40 -08:00
Zhicheng Wang
de6c6445af fix(compiler): Don't strip /*# sourceURL ... */ (#16088)
Currently, `shimCssText` only keep `/*# sourceMappingUrl ... */` comments and strip `/*# sourceURL ... */` comments. So, Chrome can't find the source maps for component style(that's created in new `style` tags)

PR Close #16088
2018-01-24 12:35:31 -08:00
Miško Hevery
54238822e6 build: merge PR to all branches per target: label (#21739)
PR Close #21739
2018-01-24 12:35:13 -08:00
Patrick Shields
8b3fbb5bf4 fix(router): don't use ParamsInheritanceStrategy in declarations (#21574)
ParamsInheritanceStrategy is internal, so any references to it from the
published .d.ts files will fail.

Fixes #21456.

PR Close #21574
2018-01-23 21:34:37 -08:00
George Kalpakas
2f61d3c320 fix(aio): remove remaining plnkr references (#20165)
PR Close #20165
2018-01-23 21:33:55 -08:00
George Kalpakas
5894f6ee1c build(aio): check for obsolete plnkr.json and missing main files (#20165)
Also, remove `plnkr.json` for `service-worker-getting-started` guide,
since it is not used and ServiceWorker cannot work correctly in
plnkr/stackblitz anyway (e.g. no build step to re-compute hashes).
A zipper might be useful and can be added in a subsequent PR, but it is
currently broken (e.g. no dependency on `@angular/service-worker`).

PR Close #20165
2018-01-23 21:33:55 -08:00
Ward Bell
6d9fcd62de build(aio): upgrade sample package.json files to jasmine@~2.8.0 (#20165)
- Update tooling to support revised testing guide (PR #20697).
- Require jasmine upgrade for examples that use marble testing.
- Copy `cli/package.json` to `testing/` and add `jasmine-marbles`.
- Resolve merge conflicts created by `NgModules` guides.

PR Close #20165
2018-01-23 21:33:55 -08:00
Jesus Rodriguez
0cbccc06dd build(aio): migrate plunker to stackblitz (#20165)
PR Close #20165
2018-01-23 21:33:52 -08:00
Victor Berchet
ed670a36fb docs: update ICU select messages to use male/female (#21713)
fixes #21694

PR Close #21713
2018-01-23 16:32:24 -08:00
Victor Berchet
8e44577df3 fix(compiler): fix ICU select messages to use male/female/other (#21713)
related to #21694

PR Close #21713
2018-01-23 16:32:24 -08:00
ghetolay
6921c20ea1 test(forms): Better description and coverage for #19256 (#21652)
fixes #21575

PR Close #21652
2018-01-23 16:31:45 -08:00
Chuck Jazdzewski
52970c09e1 fix(compiler-cli): do not fold errors past calls in the collector (#21708)
Folding errors passed calls prevented the static reflector from
begin able to ignore errors in annotations it doesn't know as
the call to the unknown annotation was elided from the metadata.

Fixes: #21273

PR Close #21708
2018-01-23 13:33:26 -08:00
JiaLi.Passion
eecdf3414e docs: fix #19989, add zone flags(blacklist/module) in guide (#21701)
PR Close #21701
2018-01-23 13:33:11 -08:00
Alex Eagle
21f766968d refactor(bazel): pass around tsconfig as a file, not a path (#21614)
this unlocks the ability to replay ts compilations with different settings

PR Close #21614
2018-01-23 10:06:05 -08:00
Alex Eagle
4b68fdce6f build: Update to latest rules_typescript. (#21675)
Fixes #21481

PR Close #21675
2018-01-22 15:34:48 -08:00
Victor Berchet
c12ea3a1f0 fix(common): A null value should remove the style on IE (#21679)
fixes #21064

PR Close #21679
2018-01-22 12:57:23 -08:00
Denny Brown
d7dbdc5c36 docs: fix stray div and reformat paragraph (#21676)
PR Close #21676
2018-01-19 20:42:04 -08:00
Olivier Combe
0112a903f9 ci: add github bot config to triage issues (#21672)
Fixes #21635
PR Close #21672
2018-01-19 20:41:15 -08:00
Yuan Gao
66bbc84127 ci(aio): do not limit size of gzip7 and gzip 9 (#21601)
PR Close #21601
2018-01-19 20:41:01 -08:00
Pete Bacon Darwin
554129d6fe feat(aio): update metatags to control search engine crawling (#21665)
The `<meta name="robots" content="noindex">` tag is used
to indicate to search engine crawlers that they should not index
the current page. This is set dynamically by the the document
viewer component to ensure that 404 and other erroring pages
are not added to the search index.

This relies upon the idea that the crawling bot will run the JS
and wait to see if this meta tag has been added or not.

Since we believe that the `googebot` will do this, we also
pre-emptively add a hard-coded noindex tag specifically for
this bot, so that if anything else fails in bootstrapping the app,
the failed page will not be added to the index.

Closes #21317

PR Close #21665
2018-01-19 20:31:45 -08:00
Chuck Jazdzewski
e32a0cabfe fix(aio): add a required comma in firebase.json (#21618)
PR Close #21618
2018-01-19 20:31:30 -08:00
Alex Eagle
c828e5627b build: Remove angular_src nested workspace (#21096)
PR Close #21096
2018-01-19 13:10:09 -08:00
Fabian Wiles
1626e74c59 docs: clarify the use of classes and interfaces in style guide (#20919)
PR Close #20919
2018-01-19 13:09:58 -08:00
Igor Minar
a15a2b46d1 ci: add "PR action: cleanup" to the bot's forbiddenLabels list (#21562)
PR Close #21562
2018-01-19 13:09:41 -08:00
George Kalpakas
379ed75593 docs: improve/simplify example for providers guide (#21589)
PR Close #21589
2018-01-19 13:09:31 -08:00
George Kalpakas
0f619896b3 docs: fix/improve example for singleton-services guide (#21589)
PR Close #21589
2018-01-19 13:09:31 -08:00
George Kalpakas
7060655806 docs: several minor NgModule guide fixes/improvements (#21589)
PR Close #21589
2018-01-19 13:09:31 -08:00
George Kalpakas
b5fc3eb9de docs: minor fixes (anchor tags, redundant whitespace, consistent code-snippets lang) (#21589)
PR Close #21589
2018-01-19 13:09:31 -08:00
Vani
451bdb9a75 docs: change titles to sentence case (#21620)
PR Close #21620
2018-01-19 13:09:25 -08:00
George Kalpakas
983ccc02ad build(aio): fix zips testing commands (#21629)
PR Close #21629
2018-01-19 13:09:17 -08:00
Olivier Combe
00f99b3c4c ci: update github bot messages (#21634)
Fixes #21633
PR Close #21634
2018-01-19 13:09:11 -08:00
Chuck Jazdzewski
ba4ea82f68 fix(compiler-cli): do not lower expressions in non-modules (#21649)
Fixes: #21651

PR Close #21649
2018-01-19 13:09:04 -08:00
Trotyl
982eb7bba8 fix(common): fallback to last defined value for named date and time formats (#21299)
closes #21282

PR Close #21299
2018-01-19 13:08:57 -08:00
Kapunahele Wong
3606c55410 docs: edit entry component FAQ (#21487)
PR Close #21487
2018-01-19 13:08:50 -08:00
Kapunahele Wong
2c65027391 docs: add server side redirect and fix NgModule FAQ links (#21487)
PR Close #21487
2018-01-19 13:08:50 -08:00
Kapunahele Wong
4ee92f14a6 docs: fix lazy loading example dir name (#21475)
PR Close #21475
2018-01-19 13:08:30 -08:00
Misko Hevery
c9b65914d3 build: add mhevery to bazel approvers (#21314)
PR Close #21314
2018-01-19 13:08:19 -08:00
Olivier Combe
02352bcd9e fix(compiler): add support for marker tags in xliff serializers (#21250)
The Xliff serializer now supports the tags `seg-source` and `mrk`, while the Xliff2 serializer now supports `mrk`.
Fixes #21078
PR Close #21250
2018-01-19 13:08:10 -08:00
Miško Hevery
0d55600fd8 Revert "fix(core): fix chained http call (#20924)"
This reverts commit 54e75766adb22e15cd9831991e8537d2ca71eaef.
2018-01-19 13:06:33 -08:00
Alex Eagle
af4eb00c91 docs: add changelog for 5.2.1 2018-01-17 09:39:46 -08:00
Alex Eagle
d3e7ebb3b4 release: cut the 5.2.1 release 2018-01-17 09:37:50 -08:00
Alex Eagle
420f5c4275 build(bazel): remove spurious file (#21455)
PR Close #21455
2018-01-17 08:24:34 -08:00
Alan Agius
b773a4ab98 docs(aio): change df-question to app-question (#21438)
closes: #21404
PR Close #21438
2018-01-17 07:20:52 -08:00
Jason Aden
55f15c54d9 docs(aio): add description and docs links for code samples (#21561)
PR Close #21561
2018-01-17 07:19:37 -08:00
Xander Garbett
4556532c26 feat(core): add binding name to content changed error (#20352)
Adding the binding name to the error message recieved by the user gives
extra context on what exactly changed. The tests are also updated to
reflect the new error message.

PR Close #20352
2018-01-17 07:17:00 -08:00
Benjamin Ingberg
54e75766ad fix(core): fix chained http call (#20924)
Fixes an issue where chained http calls would prematurely call
testability whenStable callbacks after the first http call.

Fixes #20921

PR Close #20924
2018-01-17 07:14:55 -08:00
Victor Berchet
d3333f04ba refactor(core): refactor WrappedValue (#20997)
- Improve `WrappedValue` by adding `unwrap` symetrical to `wrap`.
- remove dead code - `ValueUnwrapper`

The property `wrapped` is an implementation details and should never be accessed
directly - use `unwrap(wrappedValue)`. Will change to protected in Angular 7.

PR Close #20997
2018-01-16 07:13:24 -08:00
ghetolay
75f8522b8d feat(forms): handle string with and without line boundary on pattern validator (#19256)
PR Close #19256
2018-01-16 07:12:14 -08:00
Pete Bacon Darwin
a771ee5d90 build(aio): move file cleaning to later in the doc gen (#21540)
Previously the generated files were cleaned out before
doc-gen began (via a yarn pre-script). This can cause a
race condition in the CLI server, which prevents the new
generated files from being picked up.

Now we delay the cleaning until the last minute to ensure
that they ar still picked up by the webpack server.

PR Close #21540
2018-01-16 07:11:01 -08:00
Alex Eagle
a4cbe3542a ci: disable integration/cli-hello-world test (#21492)
it is non-hermetic and breaks often due to unpinned dependencies.

PR Close #21492
2018-01-12 14:57:45 -08:00
Keen Yee Liau
cc9419d1ca fix(language-service): Clear caches when program changes (#21337)
This commit fixes a bug whereby the caches are not cleared when the
program changes. This subsequently produces the incorrect error of
'Component ... is not included in a module ...'.

PR Close #19405

PR Close #21337
2018-01-12 14:43:34 -08:00
Chuck Jazdzewski
d5393c7f91 docs(compiler): document the $any type cast function (#20968)
Closes #20966

PR Close #20968
2018-01-12 14:38:42 -08:00
Pete Bacon Darwin
71dd92bbb8 style(aio): enforce strict TypeScript checks (#21342)
Closes #20646

PR Close #21342
2018-01-12 14:37:02 -08:00
Olivier Combe
977978edb5 test(common): make date pipe tests work in more timezones (#21379)
Fixes #21112
PR Close #21379
2018-01-12 13:50:06 -08:00
George Kalpakas
eb70966065 fix(aio): correctly handle redirects (#21416)
- Fixes handling of some redirects by the ServiceWorker.
- Fixes redirect for old `NgFor` to new `NgForOf` URL.

Fixes #21318

PR Close #21416
2018-01-12 13:49:10 -08:00
George Kalpakas
cf4bea587d ci(aio): run e2e tests in production mode (#21470)
This will enable catching errors introduced by build optimizations that
do not appear in `development` mode.

Fixes #21446

PR Close #21470
2018-01-12 13:46:00 -08:00
Olivier Combe
8e9cd57951 fix(common): set correct timezone for ISO8601 dates in Safari (#21506)
Fixes #21491
PR Close #21506
2018-01-12 13:21:11 -08:00
Olivier Combe
f23896f519 docs: fix release schedule date (#21474)
PR Close #21474
2018-01-12 12:10:26 -08:00
Olivier Combe
0e59d18fc2 ci: add angular robot config file (#21489)
PR Close #21489
2018-01-12 10:12:28 -08:00
Igor Minar
54c8a321a9 ci: update pullapprove rules now that we allow selfapprovals (#21494)
PR Close #21494
2018-01-12 10:11:37 -08:00
Pete Bacon Darwin
9005a6f3cd feat(aio): implement survey notification link (#21371)
Closes #21094

PR Close #21371
2018-01-12 10:06:08 -08:00
Marc Laval
3c6a5063f7 fix(benchpress): should still support selenium_webdriver < 3.6.0 (#21477)
PR Close #21477
2018-01-11 10:54:11 -08:00
Kapunahele Wong
b49d54e606 docs(forms): clarify note in reactive forms doc (#21134)
PR Close #21134
2018-01-11 07:04:15 -08:00
Olivier Combe
55fd82e587 docs: fix release schedule date (#21469)
PR Close #21469
2018-01-11 06:45:49 -08:00
Kara Erickson
7f3d0bbf97 build: update polyfill size (#21461)
PR Close #21461
2018-01-10 17:07:32 -08:00
Kapunahele Wong
3db02d244a docs: fix lazy-loading example (#20306)
PR Close #20306
2018-01-10 16:26:28 -08:00
Kapunahele Wong
50b605686e docs: fix yarn.lock (#20306)
PR Close #20306
2018-01-10 16:26:28 -08:00
Kapunahele Wong
64d4aafbc7 docs(aio): add NgModule docs (#20306)
PR Close #20306
2018-01-10 16:26:28 -08:00
Chuck Jazdzewski
a931a419fa fix(compiler): make .ngsummary.json files idempotent (#21448)
Fixes: #21432

PR Close #21448
2018-01-10 16:21:32 -08:00
Miško Hevery
6a97b5b722 docs(ivy): add Ivy as a recognized label (#21428)
PR Close #21428
2018-01-10 15:32:59 -08:00
Igor Minar
aad1126446 docs: update the release schedule with v6 info (#21435)
Fixes #20649

PR Close #21435
2018-01-10 15:20:27 -08:00
George Kalpakas
7b463df52b fix(aio): preserve static Observable methods (#21351)
PR Close #21351
2018-01-10 14:30:42 -08:00
Igor Minar
a2432c9f10 build(aio): turn on namedChunks option to make debugging, profiling and tracking easier (#21351)
PR Close #21351
2018-01-10 14:30:42 -08:00
Igor Minar
e500484ccc build(aio): upgrade to angular@5.2.0 (#21351)
-rw-r--r--  1 iminar  eng   72498 Jan  9 22:20 dist/0.0d2802b63e9f4f4615cd.chunk.js
-rw-r--r--  1 iminar  eng   14872 Jan  9 22:20 dist/4.c719ac5645940382cdce.chunk.js
-rw-r--r--  1 iminar  eng    1558 Jan  9 22:20 dist/inline.cb4b7a639193fc489d3b.bundle.js
-rw-r--r--  1 iminar  eng  454654 Jan  9 22:20 dist/main.b65f32ffc92e075e64e0.bundle.js
-rw-r--r--  1 iminar  eng   40264 Jan  9 22:20 dist/polyfills.87edf5d695f14a29bf91.bundle.js
-rw-r--r--  1 iminar  eng   54001 Jan  9 22:20 dist/worker-basic.min.js

PR Close #21351
2018-01-10 14:30:42 -08:00
Igor Minar
a0dcb0b828 ci(aio): track sizes of all js files (#21351)
PR Close #21351
2018-01-10 14:30:42 -08:00
Igor Minar
c4b71920d1 build(aio): upgrade to angular@5.2.0-rc.0 (#21351)
Because of c2b3792 one of the test assertions had to be adjusted (more info: https://github.com/angular/angular/pull/21351#issuecomment-356083940).

PR Close #21351
2018-01-10 14:30:42 -08:00
Alex Eagle
650f5fb5c7 build(bazel): fix merge conflict between c4f02e2 and ef956a2 (#21453)
PR Close #21453
2018-01-10 12:57:38 -08:00
Chuck Jazdzewski
c32e83334b fix(compiler): cache external reference resolution (#21359)
Cache reference resolution for external references as finding
the declaration of a symbol is expensive and does not change
for a program once created.

This resolves a signficant performance regression in the langauge
service.

PR Close #21359
2018-01-10 12:34:07 -08:00
Olivier Combe
7bdd9aecbd build: upgrade yarn to 1.3.2 (#21406)
Fixes #20566
PR Close #21406
2018-01-10 12:32:47 -08:00
Alex Eagle
5ede67c345 build: move repeated tsconfig attributes to a macro (#20964)
This helps ensure we use the same tsconfig.json file for all compilations.
Next steps are to make it the same tsconfig.json file used by the editor

PR Close #20964
2018-01-10 12:30:44 -08:00
Suguru Inatomi
31b671ab54 fix(aio): support multibyte character in heading (#21414)
PR Close #21414
2018-01-10 12:25:45 -08:00
Alex Eagle
3804d20b6d docs(bazel): document an installation issue (#21401)
observed by Igor on a Mac

PR Close #21401
2018-01-10 12:24:03 -08:00
Igor Minar
0a5a87887e build(aio): upgrade to uglify-es@3.3.5 (#21350)
This should fix the size regression spotted via the previous commit.

-rw-r--r--  1 iminar  eng   72498 Jan  8 00:03 dist/0.86a7a08f7866e6cdc36f.chunk.js
-rw-r--r--  1 iminar  eng   14872 Jan  8 00:03 dist/4.c719ac5645940382cdce.chunk.js
-rw-r--r--  1 iminar  eng    1558 Jan  8 00:03 dist/inline.7722895d8c844f710bcd.bundle.js
-rw-r--r--  1 iminar  eng  453905 Jan  8 00:03 dist/main.faff0e2da95443f759f5.bundle.js
-rw-r--r--  1 iminar  eng   40264 Jan  8 00:03 dist/polyfills.87edf5d695f14a29bf91.bundle.js
-rw-r--r--  1 iminar  eng   54001 Jan  8 00:03 dist/worker-basic.min.js

PR Close #21350
2018-01-10 12:22:12 -08:00
Igor Minar
c46afce0f5 build(aio): upgrade to @angular/cli@1.6.3 (#21350)
This change introduces a size regression of 9kb for main.js :-(

I filed an issue for this: https://github.com/angular/angular-cli/issues/9108

-rw-r--r--  1 iminar  eng   72546 Jan  5 19:27 dist.cli-1.6.3/0.86a7a08f7866e6cdc36f.chunk.js
-rw-r--r--  1 iminar  eng   14893 Jan  5 19:27 dist.cli-1.6.3/4.c719ac5645940382cdce.chunk.js
-rw-r--r--  1 iminar  eng    1558 Jan  5 19:27 dist.cli-1.6.3/inline.7722895d8c844f710bcd.bundle.js
-rw-r--r--  1 iminar  eng  466484 Jan  5 19:27 dist.cli-1.6.3/main.faff0e2da95443f759f5.bundle.js
-rw-r--r--  1 iminar  eng   40363 Jan  5 19:27 dist.cli-1.6.3/polyfills.87edf5d695f14a29bf91.bundle.js
-rw-r--r--  1 iminar  eng   54001 Jan  5 19:27 dist.cli-1.6.3/worker-basic.min.js

PR Close #21350
2018-01-10 12:22:12 -08:00
courtneypattison
76c781fd37 docs(router): remove extra brackets (#21294)
PR Close #21294
2018-01-10 12:20:48 -08:00
Alex Rickabaugh
51eb3d418e fix(service-worker): properly handle invalid hashes in all scenarios (#21288)
When the SW fetches URLs listed in a manifest with hashes, it checks
the content hash against the manifest to make sure it has the correct
version of the URL. In the event of a mismatch, the SW is supposed to
consider the manifest invalid, and avoid using it. There are 3 cases
to consider by which this can happen.

Case 1: during the initial SW installation, a manifest is activated
without waiting for every URL to be fully loaded. In the background,
every prefetch URL listed by the manifest is requested and cached.
One such prefetch request could fail the hash test, and cause the
manifest to be treated as invalid. In such a case, the SW should
enter a state of EXISTING_CLIENTS_ONLY, as the latest manifest is
invalid.

This case works today.

Case 2: during the initial SW installation, as in Case 1, a manifest
is activated without waiting for each URL to fully load. However,
it's possible that the application could request a URL with a bad
hash before background initialization tries to load that URL. This
happens if, for example, the application has a broken index.html.

In this case, the SW should enter a state of EXISTING_CLIENTS_ONLY,
and serve the request from the network instead.

What happens today is that the internal error escapes the SW and
is returned as a rejected Promise to respondWith(), causing a
browser-level error that the site cannot be loaded, breaking the
site.

This change allows the SW to detect the error and enter the correct
state, falling back on the network if needed.

Case 3: during checkForUpdate(), the SW will try to fully cache the
new update before making it the latest version. Failure here is
complicated - if the page fails to load due to transient network
conditions (timeouts, 500s, etc), then it makes sense to continue
serving the existing cached version, and attempt to activate the
update on the next cycle.

If the page fails due to non-transient conditions though (400 error,
hash mismatch, etc), then the SW should consider the updated
manifest invalid, and enter a state of EXISTING_CLIENTS_ONLY.

Currently, all errors are treated as transient.

This change causes the SW to treat all errors during updates as
non-transient, which can cause the SW to unnecessarily enter a
safe mode. A future change can allow the SW to remain in normal mode
if the error is provably transient.

PR Close #21288
2018-01-10 12:18:55 -08:00
cexbrayat
48c18985cc fix(animations): fix increment/decrement aliases example (#18323)
PR Close #18323
2018-01-10 12:07:24 -08:00
Misko Hevery
167cbed266 build: add bazel rulse for benchmarks (#21436)
PR Close #21436
2018-01-10 12:01:17 -08:00
Matias Niemelä
70e8802540 docs(core): provide note for unit test changes with the ng-star-inserted className (#21450)
PR Close #21450
2018-01-10 11:14:28 -08:00
1847 changed files with 61775 additions and 22954 deletions

30
.circleci/bazel.rc Normal file
View File

@ -0,0 +1,30 @@
# These options are enabled when running on CI
# We do this by copying this file to /etc/bazel.bazelrc at the start of the build.
# See remote cache documentation in /docs/BAZEL.md
# Don't be spammy in the logs
build --noshow_progress
# Don't run manual tests
test --test_tag_filters=-manual
# Enable experimental CircleCI bazel remote cache proxy
# See remote cache documentation in /docs/BAZEL.md
build --experimental_remote_spawn_cache --remote_rest_cache=http://localhost:7643
# Prevent unstable environment variables from tainting cache keys
build --experimental_strict_action_env
# Save downloaded repositories such as the go toolchain
# This directory can then be included in the CircleCI cache
# It should save time running the first build
build --experimental_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
# https://circleci.com/docs/2.0/configuration-reference/#resource_class
build --local_resources=14336,8.0,1.0
# Retry in the event of flakes, eg. https://circleci.com/gh/angular/angular/31309
test --flaky_test_attempts=2

View File

@ -13,7 +13,14 @@
# If you change the `docker_image` version, also change the `cache_key` suffix and the version of # If you change the `docker_image` version, also change the `cache_key` suffix and the version of
# `com_github_bazelbuild_buildtools` in the `/WORKSPACE` file. # `com_github_bazelbuild_buildtools` in the `/WORKSPACE` file.
var_1: &docker_image angular/ngcontainer:0.1.0 var_1: &docker_image angular/ngcontainer:0.1.0
var_2: &cache_key angular-{{ .Branch }}-{{ checksum "yarn.lock" }}-0.1.0 var_2: &cache_key v2-angular-{{ .Branch }}-{{ checksum "yarn.lock" }}-0.1.0
# See remote cache documentation in /docs/BAZEL.md
var_3: &setup-bazel-remote-cache
run:
name: Start up bazel remote cache proxy
command: ~/bazel-remote-proxy -backend circleci://
background: true
# Settings common to each job # Settings common to each job
anchor_1: &job_defaults anchor_1: &job_defaults
@ -34,6 +41,7 @@ jobs:
steps: steps:
- checkout: - checkout:
<<: *post_checkout <<: *post_checkout
# Check BUILD.bazel formatting before we have a node_modules directory # Check BUILD.bazel formatting before we have a node_modules directory
# Then we don't need any exclude pattern to avoid checking those files # Then we don't need any exclude pattern to avoid checking those files
- run: 'buildifier -mode=check $(find . -type f \( -name BUILD.bazel -or -name BUILD \)) || - run: 'buildifier -mode=check $(find . -type f \( -name BUILD.bazel -or -name BUILD \)) ||
@ -42,6 +50,7 @@ jobs:
- run: 'find . -type f -name "*.bzl" | - run: 'find . -type f -name "*.bzl" |
xargs java -jar /usr/local/bin/Skylint_deploy.jar || xargs java -jar /usr/local/bin/Skylint_deploy.jar ||
(echo -e "\n.bzl files have lint errors. Please run ''yarn skylint''"; exit 1)' (echo -e "\n.bzl files have lint errors. Please run ''yarn skylint''"; exit 1)'
- restore_cache: - restore_cache:
key: *cache_key key: *cache_key
@ -50,24 +59,55 @@ jobs:
build: build:
<<: *job_defaults <<: *job_defaults
resource_class: large resource_class: xlarge
steps: steps:
- checkout: - checkout:
<<: *post_checkout <<: *post_checkout
# See remote cache documentation in /docs/BAZEL.md
- run: .circleci/setup_cache.sh
- run: sudo cp .circleci/bazel.rc /etc/bazel.bazelrc
- *setup-bazel-remote-cache
- restore_cache: - restore_cache:
key: *cache_key key: *cache_key
- run: ls /home/circleci/bazel_repository_cache || true
- run: bazel info release - run: bazel info release
- run: bazel run @yarn//:yarn - run: bazel run @yarn//:yarn
# Use bazel query so that we explicitly ask for all buildable targets to be built as well # Use bazel query so that we explicitly ask for all buildable targets to be built as well
# This avoids waiting for a build command to finish before running the first test # This avoids waiting for a build command to finish before running the first test
# See https://github.com/bazelbuild/bazel/issues/4257 # See https://github.com/bazelbuild/bazel/issues/4257
- run: bazel query --output=label '//packages/... union @angular//...' | xargs bazel test --config=ci - run: bazel query --output=label '//modules/... union //packages/... union //tools/...' | xargs bazel test
- save_cache: - save_cache:
key: *cache_key key: *cache_key
paths: paths:
- "node_modules" - "node_modules"
- "~/bazel_repository_cache"
aio_preview:
<<: *job_defaults
environment:
AIO_SNAPSHOT_ARTIFACT_PATH: &aio_preview_artifact_path 'aio/tmp/snapshot.tgz'
steps:
- checkout:
<<: *post_checkout
- restore_cache:
key: *cache_key
- run: ./aio/scripts/build-artifacts.sh $AIO_SNAPSHOT_ARTIFACT_PATH
- store_artifacts:
path: *aio_preview_artifact_path
# The `destination` needs to be kept in synch with the value of
# `AIO_ARTIFACT_PATH` in `aio/aio-builds-setup/Dockerfile`
destination: aio/dist/aio-snapshot.tgz
aio_monitoring:
<<: *job_defaults
steps:
- checkout:
<<: *post_checkout
- restore_cache:
key: *cache_key
- run: xvfb-run --auto-servernum ./aio/scripts/test-production.sh
workflows: workflows:
version: 2 version: 2
@ -75,3 +115,17 @@ workflows:
jobs: jobs:
- lint - lint
- build - build
- aio_preview
aio_monitoring:
jobs:
- aio_monitoring
triggers:
- schedule:
cron: "0 0 * * *"
filters:
branches:
only:
- master
notify:
webhooks:
- url: https://ngbuilds.io/circle-build

11
.circleci/setup_cache.sh Executable file
View File

@ -0,0 +1,11 @@
#!/bin/sh
# Install bazel remote cache proxy
# This is temporary until the feature is no longer experimental on CircleCI.
# See remote cache documentation in /docs/BAZEL.md
set -u -e
readonly DOWNLOAD_URL="https://5-116431813-gh.circle-artifacts.com/0/pkg/bazel-remote-proxy-$(uname -s)_$(uname -m)"
curl --fail -o ~/bazel-remote-proxy "$DOWNLOAD_URL"
chmod +x ~/bazel-remote-proxy

View File

@ -25,7 +25,7 @@ ISSUES MISSING IMPORTANT INFORMATION MAY BE CLOSED WITHOUT INVESTIGATION.
## Minimal reproduction of the problem with instructions ## Minimal reproduction of the problem with instructions
<!-- <!--
For bug reports please provide the *STEPS TO REPRODUCE* and if possible a *MINIMAL DEMO* of the problem via For bug reports please provide the *STEPS TO REPRODUCE* and if possible a *MINIMAL DEMO* of the problem via
https://plnkr.co or similar (you can use this template as a starting point: http://plnkr.co/edit/tpl:AvJOMERrnz94ekVua0u5). https://stackblitz.com or similar (you can use this template as a starting point: https://stackblitz.com/fork/angular-gitter).
--> -->
## What is the motivation / use case for changing the behavior? ## What is the motivation / use case for changing the behavior?

100
.github/angular-robot.yml vendored Normal file
View File

@ -0,0 +1,100 @@
# Configuration for angular-robot
# options for the merge plugin
merge:
# the status will be added to your pull requests
status:
# set to true to disable
disabled: false
# the name of the status
context: "ci/angular: merge status"
# text to show when all checks pass
successText: "All checks passed!"
# text to show when some checks are failing
failureText: "The following checks are failing:"
# 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
# the name of the status
context: "google3"
# text to show when the status is pending
pendingDesc: "Googler: test this change in google3 http://go/angular-g3sync"
# text to show when the status is success
successDesc: "Does not affect google3"
# 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:
- "BUILD.bazel"
- "LICENSE"
- "WORKSPACE"
- "modules/**"
- "packages/**"
# list of patterns to ignore for the files changed by the PR
exclude:
- "packages/language-service/**"
- "**/.gitignore"
- "**/.gitkeep"
# 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!"
# label to monitor
mergeLabel: "PR action: merge"
# list of checks that will determine if the merge label can be added
checks:
# whether the PR shouldn't have a conflict with the base branch
noConflict: true
# list of labels that a PR needs to have, checked with a regexp (e.g. "PR target:" will work for the label "PR target: master")
requiredLabels:
- "PR target: *"
- "cla: yes"
# list of labels that a PR shouldn't have, checked after the required labels with a regexp
forbiddenLabels:
- "PR target: TBD"
- "PR action: cleanup"
- "PR action: review"
- "PR state: blocked"
- "cla: no"
# list of PR statuses that need to be successful
requiredStatuses:
- "continuous-integration/travis-ci/pr"
- "code-review/pullapprove"
- "ci/circleci: build"
- "ci/circleci: lint"
# the comment that will be added when the merge label is added despite failing checks, leave empty or set to false to disable
# {{MERGE_LABEL}} will be replaced by the value of the mergeLabel option
# {{PLACEHOLDER}} will be replaced by the list of failing checks
mergeRemovedComment: "I see that you just added the `{{MERGE_LABEL}}` label, but the following checks are still failing:
\n{{PLACEHOLDER}}
\n
\n**If you want your PR to be merged, it has to pass all the CI checks.**
\n
\nIf you can't get the PR to a green state due to flakes or broken master, please try rebasing to master and/or restarting the CI job. If that fails and you believe that the issue is not due to your change, please contact the caretaker and ask for help."
# options for the triage plugin
triage:
# number of the milestone to apply when the issue is triaged
defaultMilestone: 82,
# arrays of labels that determine if an issue is triaged
triagedLabels:
-
- "type: bug"
- "severity*"
- "freq*"
- "comp: *"
-
- "type: feature"
- "comp: *"
-
- "type: refactor"
- "comp: *"
-
- "type: RFC / Discussion / question"
- "comp: *"

View File

@ -10,11 +10,11 @@
# brocco - Mike Brocchi # brocco - Mike Brocchi
# chuckjaz - Chuck Jazdzewski # chuckjaz - Chuck Jazdzewski
# filipesilva - Filipe Silva # filipesilva - Filipe Silva
# Foxandxss - Jesús Rodríguez
# gkalpak - George Kalpakas # gkalpak - George Kalpakas
# hansl - Hans Larsen # hansl - Hans Larsen
# IgorMinar - Igor Minar # IgorMinar - Igor Minar
# jasonaden - Jason Aden # jasonaden - Jason Aden
# kapunahelewong - Kapunahele Wong
# kara - Kara Erickson # kara - Kara Erickson
# matsko - Matias Niemelä # matsko - Matias Niemelä
# mhevery - Misko Hevery # mhevery - Misko Hevery
@ -44,6 +44,7 @@ groups:
all: all:
users: all users: all
required: 1 required: 1
rejection_value: -999
# In this group, your self-approval does not count # In this group, your self-approval does not count
author_approval: author_approval:
auto: false auto: false
@ -91,7 +92,8 @@ groups:
users: users:
- alexeagle #primary - alexeagle #primary
- chuckjaz - chuckjaz
- IgorMinar - IgorMinar #fallback
- mhevery
- vikerman #fallback - vikerman #fallback
build-and-ci: build-and-ci:
@ -127,8 +129,9 @@ groups:
files: files:
- "packages/core/*" - "packages/core/*"
users: users:
- chuckjaz #primary - mhevery #primary
- mhevery - chuckjaz
- kara
- vicb - vicb
- IgorMinar #fallback - IgorMinar #fallback
@ -139,7 +142,6 @@ groups:
- "packages/platform-browser/animations/*" - "packages/platform-browser/animations/*"
users: users:
- matsko #primary - matsko #primary
- chuckjaz #fallback
- mhevery #fallback - mhevery #fallback
- IgorMinar #fallback - IgorMinar #fallback
@ -171,6 +173,7 @@ groups:
- hansl - hansl
- filipesilva #fallback - filipesilva #fallback
- brocco #fallback - brocco #fallback
- IgorMinar #fallback
compiler-cli: compiler-cli:
conditions: conditions:
@ -216,9 +219,8 @@ groups:
- "packages/common/http/*" - "packages/common/http/*"
- "packages/http/*" - "packages/http/*"
users: users:
- vikerman #primary - alxhub #primary
- alxhub - IgorMinar
- IgorMinar #fallback
- mhevery #fallback - mhevery #fallback
language-service: language-service:
@ -237,7 +239,7 @@ groups:
files: files:
- "packages/router/*" - "packages/router/*"
users: users:
- jasonaden - jasonaden #primary
- vicb - vicb
- IgorMinar #fallback - IgorMinar #fallback
- mhevery #fallback - mhevery #fallback
@ -268,8 +270,7 @@ groups:
- "packages/platform-server/*" - "packages/platform-server/*"
users: users:
- vikerman #primary - vikerman #primary
# needs secondary - alxhub #secondary
- alxhub
- vicb - vicb
- IgorMinar #fallback - IgorMinar #fallback
- mhevery #fallback - mhevery #fallback
@ -327,11 +328,11 @@ groups:
- "aio/content/navigation.json" - "aio/content/navigation.json"
- "aio/content/license.md" - "aio/content/license.md"
users: users:
- kapunahelewong
- stephenfluin - stephenfluin
- Foxandxss
- petebacondarwin - petebacondarwin
- gkalpak - gkalpak
- IgorMinar #fallback - IgorMinar
- mhevery #fallback - mhevery #fallback
angular.io-marketing: angular.io-marketing:
@ -345,5 +346,5 @@ groups:
- stephenfluin - stephenfluin
- petebacondarwin - petebacondarwin
- gkalpak - gkalpak
- IgorMinar #fallback - IgorMinar
- mhevery #fallback - mhevery #fallback

View File

@ -1,7 +1,5 @@
language: node_js language: node_js
# Work-around for https://github.com/travis-ci/travis-ci/issues/8836#issuecomment-356362524. sudo: false
# (Restore `sudo: false` once that is resolved.)
sudo: required
dist: trusty dist: trusty
node_js: node_js:
- '8.9.1' - '8.9.1'
@ -58,7 +56,6 @@ env:
- CI_MODE=aio - CI_MODE=aio
- CI_MODE=aio_e2e AIO_SHARD=0 - CI_MODE=aio_e2e AIO_SHARD=0
- CI_MODE=aio_e2e AIO_SHARD=1 - CI_MODE=aio_e2e AIO_SHARD=1
- CI_MODE=bazel
matrix: matrix:
fast_finish: true fast_finish: true

View File

@ -24,9 +24,7 @@ filegroup(
"typescript", "typescript",
"zone.js", "zone.js",
"tsutils", "tsutils",
"@types/jasmine", "@types",
"@types/node",
"@types/source-map",
"tsickle", "tsickle",
"hammerjs", "hammerjs",
"protobufjs", "protobufjs",
@ -34,6 +32,7 @@ filegroup(
"reflect-metadata", "reflect-metadata",
"source-map-support", "source-map-support",
"minimist", "minimist",
"tslib",
] for ext in [ ] for ext in [
"*.js", "*.js",
"*.json", "*.json",

View File

@ -1,3 +1,177 @@
<a name="5.2.11"></a>
## [5.2.11](https://github.com/angular/angular/compare/5.2.10...5.2.11) (2018-05-16)
### Bug Fixes
* **service-worker:** correctly handle requests with empty `clientId` ([#23625](https://github.com/angular/angular/issues/23625)) ([bc27d4a](https://github.com/angular/angular/commit/bc27d4a)), closes [#23526](https://github.com/angular/angular/issues/23526)
<a name="5.2.10"></a>
## [5.2.10](https://github.com/angular/angular/compare/5.2.9...5.2.10) (2018-04-16)
### Bug Fixes
* **animations:** avoid animation insertions during router back/refresh ([#21977](https://github.com/angular/angular/issues/21977)) ([641cc49](https://github.com/angular/angular/commit/641cc49)), closes [#19712](https://github.com/angular/angular/issues/19712)
* **common:** properly take className changes into account ([#21937](https://github.com/angular/angular/issues/21937)) ([54e9108](https://github.com/angular/angular/commit/54e9108)), closes [#21932](https://github.com/angular/angular/issues/21932)
* **compiler:** fix support for html-like text in translatable attributes ([#23053](https://github.com/angular/angular/issues/23053)) ([4f7c369](https://github.com/angular/angular/commit/4f7c369))
* **compiler-cli:** emit correct css string escape sequences ([#22776](https://github.com/angular/angular/issues/22776)) ([db0afa9](https://github.com/angular/angular/commit/db0afa9))
* **forms:** improve error message for invalid value accessors ([#22731](https://github.com/angular/angular/issues/22731)) ([dd61595](https://github.com/angular/angular/commit/dd61595))
* **service-worker:** add badge to NOTIFICATION_OPTION_NAMES ([#23241](https://github.com/angular/angular/issues/23241)) ([7b23983](https://github.com/angular/angular/commit/7b23983)), closes [#23196](https://github.com/angular/angular/issues/23196)
* **service-worker:** do not enter degraded mode when offline ([#22883](https://github.com/angular/angular/issues/22883)) ([ae9c25f](https://github.com/angular/angular/commit/ae9c25f)), closes [#21636](https://github.com/angular/angular/issues/21636)
* **service-worker:** fix LruList bugs ([#22769](https://github.com/angular/angular/issues/22769)) ([65f8943](https://github.com/angular/angular/commit/65f8943)), closes [#22218](https://github.com/angular/angular/issues/22218) [#22768](https://github.com/angular/angular/issues/22768)
* **service-worker:** ignore invalid `only-if-cached` requests ([#22883](https://github.com/angular/angular/issues/22883)) ([0d4fe38](https://github.com/angular/angular/commit/0d4fe38)), closes [#22362](https://github.com/angular/angular/issues/22362)
* **upgrade:** correctly handle downgraded `OnPush` components ([#22209](https://github.com/angular/angular/issues/22209)) ([f43fba6](https://github.com/angular/angular/commit/f43fba6)), closes [#14286](https://github.com/angular/angular/issues/14286)
* **upgrade:** propagate return value of resumeBootstrap ([#22754](https://github.com/angular/angular/issues/22754)) ([ae76eec](https://github.com/angular/angular/commit/ae76eec)), closes [#22723](https://github.com/angular/angular/issues/22723)
* **upgrade:** two-way binding and listening for event ([#22772](https://github.com/angular/angular/issues/22772)) ([5391f96](https://github.com/angular/angular/commit/5391f96)), closes [#22734](https://github.com/angular/angular/issues/22734)
<a name="5.2.9"></a>
## [5.2.9](https://github.com/angular/angular/compare/5.2.8...5.2.9) (2018-03-14)
### Bug Fixes
* **platform-server:** add styles to elements correctly ([#22527](https://github.com/angular/angular/issues/22527)) ([fc6dfc2](https://github.com/angular/angular/commit/fc6dfc2))
* **router:** correct over-encoding of URL fragment ([#22687](https://github.com/angular/angular/issues/22687)) ([86517f2](https://github.com/angular/angular/commit/86517f2))
<a name="5.2.8"></a>
## [5.2.8](https://github.com/angular/angular/compare/5.2.7...5.2.8) (2018-03-07)
### Bug Fixes
* **platform-server:** generate correct stylings for camel case names ([#22263](https://github.com/angular/angular/issues/22263)) ([de02a7a](https://github.com/angular/angular/commit/de02a7a)), closes [#19235](https://github.com/angular/angular/issues/19235)
* **router:** don't mutate route configs ([#22358](https://github.com/angular/angular/issues/22358)) ([8f0a064](https://github.com/angular/angular/commit/8f0a064)), closes [#22203](https://github.com/angular/angular/issues/22203)
* **router:** fix URL serialization so special characters are only encoded where needed ([#22337](https://github.com/angular/angular/issues/22337)) ([789a47e](https://github.com/angular/angular/commit/789a47e)), closes [#10280](https://github.com/angular/angular/issues/10280)
* **upgrade:** correctly destroy nested downgraded component ([#22400](https://github.com/angular/angular/issues/22400)) ([4aef9de](https://github.com/angular/angular/commit/4aef9de)), closes [#22392](https://github.com/angular/angular/issues/22392)
* **upgrade:** correctly handle `=` bindings in `[@angular](https://github.com/angular)/upgrade` ([#22167](https://github.com/angular/angular/issues/22167)) ([6638390](https://github.com/angular/angular/commit/6638390))
* **upgrade:** fix empty transclusion content with AngularJS@>=1.5.8 ([#22167](https://github.com/angular/angular/issues/22167)) ([a9a0e27](https://github.com/angular/angular/commit/a9a0e27)), closes [#22175](https://github.com/angular/angular/issues/22175)
<a name="5.2.7"></a>
## [5.2.7](https://github.com/angular/angular/compare/5.2.6...5.2.7) (2018-02-28)
### Bug Fixes
* **platform-server:** generate correct stylings for camel case names ([#22263](https://github.com/angular/angular/issues/22263)) ([de02a7a](https://github.com/angular/angular/commit/de02a7a)), closes [#19235](https://github.com/angular/angular/issues/19235)
* **router:** don't mutate route configs ([#22358](https://github.com/angular/angular/issues/22358)) ([8f0a064](https://github.com/angular/angular/commit/8f0a064)), closes [#22203](https://github.com/angular/angular/issues/22203)
* **upgrade:** correctly destroy nested downgraded component ([#22400](https://github.com/angular/angular/issues/22400)) ([4aef9de](https://github.com/angular/angular/commit/4aef9de)), closes [#22392](https://github.com/angular/angular/issues/22392)
* **upgrade:** correctly handle `=` bindings in `[@angular](https://github.com/angular)/upgrade` ([#22167](https://github.com/angular/angular/issues/22167)) ([6638390](https://github.com/angular/angular/commit/6638390))
* **upgrade:** fix empty transclusion content with AngularJS@>=1.5.8 ([#22167](https://github.com/angular/angular/issues/22167)) ([a9a0e27](https://github.com/angular/angular/commit/a9a0e27)), closes [#22175](https://github.com/angular/angular/issues/22175)
<a name="5.2.6"></a>
## [5.2.6](https://github.com/angular/angular/compare/5.2.5...5.2.6) (2018-02-22)
### Bug Fixes
* **common:** correct mapping of Observable methods ([#20518](https://github.com/angular/angular/issues/20518)) ([ce5e8fa](https://github.com/angular/angular/commit/ce5e8fa)), closes [#20516](https://github.com/angular/angular/issues/20516)
* **common:** then and else template might be set to null ([#22298](https://github.com/angular/angular/issues/22298)) ([af6a056](https://github.com/angular/angular/commit/af6a056))
* **compiler-cli:** add missing entry point to package, update tsickle ([#22295](https://github.com/angular/angular/issues/22295)) ([c5418c7](https://github.com/angular/angular/commit/c5418c7))
* **core:** properly handle function without prototype in reflector ([#22284](https://github.com/angular/angular/issues/22284)) ([5ec38f2](https://github.com/angular/angular/commit/5ec38f2)), closes [#19978](https://github.com/angular/angular/issues/19978)
* **core:** support metadata reflection for native class types ([#22356](https://github.com/angular/angular/issues/22356)) ([ee91de9](https://github.com/angular/angular/commit/ee91de9)), closes [#21731](https://github.com/angular/angular/issues/21731)
<a name="5.2.5"></a>
## [5.2.5](https://github.com/angular/angular/compare/5.2.4...5.2.5) (2018-02-14)
### Bug Fixes
* **aio:** update Firebase redirects and SW routes ([#21763](https://github.com/angular/angular/pull/21763)) ([#22104](https://github.com/angular/angular/pull/22104)) ([15ff7ba](https://github.com/angular/angular/commit/15ff7ba)), closes [#21377](https://github.com/angular/angular/issues/21377)
* **bazel:** allow TS to read ambient typings ([#21876](https://github.com/angular/angular/issues/21876)) ([d57fd0b](https://github.com/angular/angular/commit/d57fd0b)), closes [#21872](https://github.com/angular/angular/issues/21872)
* **bazel:** improve error message for missing assets ([#22096](https://github.com/angular/angular/issues/22096)) ([c5ec8d9](https://github.com/angular/angular/commit/c5ec8d9)), closes [#22095](https://github.com/angular/angular/issues/22095)
* **common:** weaken AsyncPipe transform signature ([#22169](https://github.com/angular/angular/issues/22169)) ([c6bdc83](https://github.com/angular/angular/commit/c6bdc83))
* **compiler:** make unary plus operator consistent to JavaScript ([#22154](https://github.com/angular/angular/issues/22154)) ([1b8ea10](https://github.com/angular/angular/commit/1b8ea10)), closes [#22089](https://github.com/angular/angular/issues/22089)
* **core:** add stacktrace in log when error during cleanup component in TestBed ([#22162](https://github.com/angular/angular/issues/22162)) ([c4f841f](https://github.com/angular/angular/commit/c4f841f))
* **core:** ensure initial value of QueryList length ([#21980](https://github.com/angular/angular/issues/21980)) ([#21982](https://github.com/angular/angular/issues/21982)) ([47b73fd](https://github.com/angular/angular/commit/47b73fd)), closes [#21980](https://github.com/angular/angular/issues/21980)
* **core:** use appropriate inert document strategy for Firefox & Safari ([#17019](https://github.com/angular/angular/issues/17019)) ([47b71d9](https://github.com/angular/angular/commit/47b71d9))
* **forms:** prevent event emission on enable/disable when emitEvent is false ([#12366](https://github.com/angular/angular/issues/12366)) ([#21018](https://github.com/angular/angular/issues/21018)) ([56b9591](https://github.com/angular/angular/commit/56b9591))
* **language-service:** correct instructions to install the language service ([#22000](https://github.com/angular/angular/issues/22000)) ([0b23573](https://github.com/angular/angular/commit/0b23573))
* **platform-browser:** support 0/false/null values in transfer_state ([#22179](https://github.com/angular/angular/issues/22179)) ([da6ab91](https://github.com/angular/angular/commit/da6ab91))
<a name="5.2.4"></a>
## [5.2.4](https://github.com/angular/angular/compare/5.2.3...5.2.4) (2018-02-07)
### Bug Fixes
* **common:** don't convert null to a string when flushing a mock request ([#21417](https://github.com/angular/angular/issues/21417)) ([c4fb696](https://github.com/angular/angular/commit/c4fb696)), closes [#20744](https://github.com/angular/angular/issues/20744)
* **core:** fix [#20582](https://github.com/angular/angular/issues/20582), don't need to wrap zone in location change listener ([#22007](https://github.com/angular/angular/issues/22007)) ([ce51ea9](https://github.com/angular/angular/commit/ce51ea9))
* **core:** fix proper propagation of subscriptions in EventEmitter ([#22016](https://github.com/angular/angular/issues/22016)) ([c6645e7](https://github.com/angular/angular/commit/c6645e7)), closes [#21999](https://github.com/angular/angular/issues/21999)
* **core:** should check Zone existance when scheduleMicroTask ([#20656](https://github.com/angular/angular/issues/20656)) ([aa9ba7f](https://github.com/angular/angular/commit/aa9ba7f))
<a name="5.2.3"></a>
## [5.2.3](https://github.com/angular/angular/compare/5.2.2...5.2.3) (2018-01-31)
### Bug Fixes
* **common:** allow HttpInterceptors to inject HttpClient ([#19809](https://github.com/angular/angular/issues/19809)) ([ed2b717](https://github.com/angular/angular/commit/ed2b717)), closes [#18224](https://github.com/angular/angular/issues/18224)
* **common:** generate closure-locale data file with exported plural functions ([#21873](https://github.com/angular/angular/issues/21873)) ([c2f5ed5](https://github.com/angular/angular/commit/c2f5ed5)), closes [#21870](https://github.com/angular/angular/issues/21870)
* **core:** fix retrieving the binding name when an expression changes ([#21814](https://github.com/angular/angular/issues/21814)) ([81d64d6](https://github.com/angular/angular/commit/81d64d6)), closes [#21735](https://github.com/angular/angular/issues/21735) [#21788](https://github.com/angular/angular/issues/21788)
* **forms:** allow FormBuilder to create controls with any formState type ([#20917](https://github.com/angular/angular/issues/20917)) ([56f3e18](https://github.com/angular/angular/commit/56f3e18)), closes [#20368](https://github.com/angular/angular/issues/20368)
* **forms:** inserting and removing controls should work in re-bound form arrays ([#21822](https://github.com/angular/angular/issues/21822)) ([fad99cc](https://github.com/angular/angular/commit/fad99cc)), closes [#21501](https://github.com/angular/angular/issues/21501)
* **language-service:** ensure correct paths are passed to TypeScript ([#21812](https://github.com/angular/angular/issues/21812)) ([250c8da](https://github.com/angular/angular/commit/250c8da))
* **language-service:** spell diagnostics correctly ([#21812](https://github.com/angular/angular/issues/21812)) ([778e6e7](https://github.com/angular/angular/commit/778e6e7))
* **router:** remove [@internal](https://github.com/internal) tag on ParamInheritanceType ([#21773](https://github.com/angular/angular/issues/21773)) ([35a0721](https://github.com/angular/angular/commit/35a0721)), closes [#21456](https://github.com/angular/angular/issues/21456)
<a name="5.2.2"></a>
## [5.2.2](https://github.com/angular/angular/compare/5.2.1...5.2.2) (2018-01-25)
### Bug Fixes
* **common:** A null value should remove the style on IE ([#21679](https://github.com/angular/angular/issues/21679)) ([c12ea3a](https://github.com/angular/angular/commit/c12ea3a)), closes [#21064](https://github.com/angular/angular/issues/21064)
* **common:** don't remove special characters when extracting CLDR data ([#21626](https://github.com/angular/angular/issues/21626)) ([a62c186](https://github.com/angular/angular/commit/a62c186))
* **common:** extract plural function from i18n locale data files for TS 2.6 ([#21626](https://github.com/angular/angular/issues/21626)) ([71f9eaa](https://github.com/angular/angular/commit/71f9eaa)), closes [#21608](https://github.com/angular/angular/issues/21608)
* **common:** fallback to last defined value for named date and time formats ([#21299](https://github.com/angular/angular/issues/21299)) ([982eb7b](https://github.com/angular/angular/commit/982eb7b)), closes [#21282](https://github.com/angular/angular/issues/21282)
* **compiler:** add support for marker tags in xliff serializers ([#21250](https://github.com/angular/angular/issues/21250)) ([02352bc](https://github.com/angular/angular/commit/02352bc)), closes [#21078](https://github.com/angular/angular/issues/21078)
* **compiler:** Don't strip `/*# sourceURL ... */` ([#16088](https://github.com/angular/angular/issues/16088)) ([de6c644](https://github.com/angular/angular/commit/de6c644))
* **compiler:** fix ICU select messages to use male/female/other ([#21713](https://github.com/angular/angular/issues/21713)) ([8e44577](https://github.com/angular/angular/commit/8e44577))
* **compiler-cli:** do not fold errors past calls in the collector ([#21708](https://github.com/angular/angular/issues/21708)) ([52970c0](https://github.com/angular/angular/commit/52970c0))
* **compiler-cli:** do not lower expressions in non-modules ([#21649](https://github.com/angular/angular/issues/21649)) ([ba4ea82](https://github.com/angular/angular/commit/ba4ea82))
* **router:** don't use ParamsInheritanceStrategy in declarations ([#21574](https://github.com/angular/angular/issues/21574)) ([8b3fbb5](https://github.com/angular/angular/commit/8b3fbb5)), closes [#21456](https://github.com/angular/angular/issues/21456)
<a name="5.2.1"></a>
## [5.2.1](https://github.com/angular/angular/compare/5.2.0...5.2.1) (2018-01-17)
### Bug Fixes
* **animations:** fix increment/decrement aliases example ([#18323](https://github.com/angular/angular/issues/18323)) ([48c1898](https://github.com/angular/angular/commit/48c1898))
* **benchpress:** should still support selenium_webdriver < 3.6.0 ([#21477](https://github.com/angular/angular/issues/21477)) ([3c6a506](https://github.com/angular/angular/commit/3c6a506))
* **common:** set correct timezone for ISO8601 dates in Safari ([#21506](https://github.com/angular/angular/issues/21506)) ([8e9cd57](https://github.com/angular/angular/commit/8e9cd57)), closes [#21491](https://github.com/angular/angular/issues/21491)
* **compiler:** cache external reference resolution ([#21359](https://github.com/angular/angular/issues/21359)) ([c32e833](https://github.com/angular/angular/commit/c32e833))
* **compiler:** make `.ngsummary.json` files idempotent ([#21448](https://github.com/angular/angular/issues/21448)) ([a931a41](https://github.com/angular/angular/commit/a931a41))
* **core:** fix chained http call ([#20924](https://github.com/angular/angular/issues/20924)) ([54e7576](https://github.com/angular/angular/commit/54e7576)), closes [#20921](https://github.com/angular/angular/issues/20921)
* **language-service:** Clear caches when program changes ([#21337](https://github.com/angular/angular/issues/21337)) ([cc9419d](https://github.com/angular/angular/commit/cc9419d)), closes [#19405](https://github.com/angular/angular/issues/19405)
* **service-worker:** properly handle invalid hashes in all scenarios ([#21288](https://github.com/angular/angular/issues/21288)) ([51eb3d4](https://github.com/angular/angular/commit/51eb3d4))
### Features
* **core:** add binding name to content changed error ([#20352](https://github.com/angular/angular/issues/20352)) ([4556532](https://github.com/angular/angular/commit/4556532))
* **forms:** handle string with and without line boundary on pattern validator ([#19256](https://github.com/angular/angular/issues/19256)) ([75f8522](https://github.com/angular/angular/commit/75f8522))
<a name="5.2.0"></a> <a name="5.2.0"></a>
# [5.2.0](https://github.com/angular/angular/compare/5.2.0-rc.0...5.2.0) (2018-01-10) # [5.2.0](https://github.com/angular/angular/compare/5.2.0-rc.0...5.2.0) (2018-01-10)
@ -9,6 +183,7 @@
* **benchpress:** work around missing events from Chrome 63 ([#21396](https://github.com/angular/angular/issues/21396)) ([fa03ae1](https://github.com/angular/angular/commit/fa03ae1)) * **benchpress:** work around missing events from Chrome 63 ([#21396](https://github.com/angular/angular/issues/21396)) ([fa03ae1](https://github.com/angular/angular/commit/fa03ae1))
* **common:** export currencies via `getCurrencySymbol` ([#20983](https://github.com/angular/angular/issues/20983)) ([fecf768](https://github.com/angular/angular/commit/fecf768)) * **common:** export currencies via `getCurrencySymbol` ([#20983](https://github.com/angular/angular/issues/20983)) ([fecf768](https://github.com/angular/angular/commit/fecf768))
Note: Due to an animation fix back in 5.1.1 ([c2b3792](https://github.com/angular/angular/commit/c2b3792a3b5fa5215fe2ef7e0ac6511086ffe4c1)) which allows for nested :leave queries to work within animation sequences, all elements that are dynamically inserted (*ngIf, *ngFor, etc…) now contain the special CSS class: “ng-star-inserted”. This may cause failures within unit tests if there are any assertions that match against element.className directly. (An easy fix for this is to match using a regular expression instead of asserting the className string directly.)
<a name="5.2.0-rc.0"></a> <a name="5.2.0-rc.0"></a>
# [5.2.0-rc.0](https://github.com/angular/angular/compare/5.2.0-beta.1...5.2.0-rc.0) (2018-01-04) # [5.2.0-rc.0](https://github.com/angular/angular/compare/5.2.0-beta.1...5.2.0-rc.0) (2018-01-04)

12
CODE_OF_CONDUCT.md Normal file
View File

@ -0,0 +1,12 @@
# Contributor Code of Conduct
## Version 0.3b-angular
As contributors and maintainers of the Angular project, we pledge to respect everyone who contributes by posting issues, updating documentation, submitting pull requests, providing feedback in comments, and any other activities.
Communication through any of Angular's channels (GitHub, Gitter, IRC, mailing lists, Google+, Twitter, etc.) must be constructive and never resort to personal attacks, trolling, public or private harassment, insults, or other unprofessional conduct.
We promise to extend courtesy and respect to everyone involved in this project regardless of gender, gender identity, sexual orientation, disability, age, race, ethnicity, religion, or level of experience. We expect anyone contributing to the Angular project to do the same.
If any member of the community violates this code of conduct, the maintainers of the Angular project may take action, removing issues, comments, and PRs or blocking accounts as deemed appropriate.
If you are subject to or witness unacceptable behavior, or have any other concerns, please email us at [conduct@angular.io](mailto:conduct@angular.io).

View File

@ -51,7 +51,7 @@ and help you to craft the change so that it is successfully accepted into the pr
Before you submit an issue, please search the issue tracker, maybe an issue for your problem already exists and the discussion might inform you of workarounds readily available. Before you submit an issue, please search the issue tracker, maybe an issue for your problem already exists and the discussion might inform you of workarounds readily available.
We want to fix all the issues as soon as possible, but before fixing a bug we need to reproduce and confirm it. In order to reproduce bugs we will systematically ask you to provide a minimal reproduction scenario using http://plnkr.co. Having a live, reproducible scenario gives us wealth of important information without going back & forth to you with additional questions like: We want to fix all the issues as soon as possible, but before fixing a bug we need to reproduce and confirm it. In order to reproduce bugs, we will systematically ask you to provide a minimal reproduction scenario using http://plnkr.co. Having a live, reproducible scenario gives us a wealth of important information without going back & forth to you with additional questions like:
- version of Angular used - version of Angular used
- 3rd-party libraries and their versions - 3rd-party libraries and their versions
@ -61,7 +61,7 @@ A minimal reproduce scenario using http://plnkr.co/ allows us to quickly confirm
We will be insisting on a minimal reproduce scenario in order to save maintainers time and ultimately be able to fix more bugs. Interestingly, from our experience users often find coding problems themselves while preparing a minimal plunk. We understand that sometimes it might be hard to extract essentials bits of code from a larger code-base but we really need to isolate the problem before we can fix it. We will be insisting on a minimal reproduce scenario in order to save maintainers time and ultimately be able to fix more bugs. Interestingly, from our experience users often find coding problems themselves while preparing a minimal plunk. We understand that sometimes it might be hard to extract essentials bits of code from a larger code-base but we really need to isolate the problem before we can fix it.
Unfortunately we are not able to investigate / fix bugs without a minimal reproduction, so if we don't hear back from you we are going to close an issue that don't have enough info to be reproduced. Unfortunately, we are not able to investigate / fix bugs without a minimal reproduction, so if we don't hear back from you we are going to close an issue that doesn't have enough info to be reproduced.
You can file new issues by filling out our [new issue form](https://github.com/angular/angular/issues/new). You can file new issues by filling out our [new issue form](https://github.com/angular/angular/issues/new).
@ -72,7 +72,7 @@ Before you submit your Pull Request (PR) consider the following guidelines:
1. Search [GitHub](https://github.com/angular/angular/pulls) for an open or closed PR 1. Search [GitHub](https://github.com/angular/angular/pulls) for an open or closed PR
that relates to your submission. You don't want to duplicate effort. that relates to your submission. You don't want to duplicate effort.
1. Please sign our [Contributor License Agreement (CLA)](#cla) before sending PRs. 1. Please sign our [Contributor License Agreement (CLA)](#cla) before sending PRs.
We cannot accept code without this. We cannot accept code without this. Make sure you sign with the primary email address of the Git identity that has been granted access to the Angular repository.
1. Fork the angular/angular repo. 1. Fork the angular/angular repo.
1. Make your changes in a new git branch: 1. Make your changes in a new git branch:
@ -173,12 +173,12 @@ The **header** is mandatory and the **scope** of the header is optional.
Any line of the commit message cannot be longer 100 characters! This allows the message to be easier Any line of the commit message cannot be longer 100 characters! This allows the message to be easier
to read on GitHub as well as in various git tools. to read on GitHub as well as in various git tools.
Footer should contain a [closing reference to an issue](https://help.github.com/articles/closing-issues-via-commit-messages/) if any. The footer should contain a [closing reference to an issue](https://help.github.com/articles/closing-issues-via-commit-messages/) if any.
Samples: (even more [samples](https://github.com/angular/angular/commits/master)) Samples: (even more [samples](https://github.com/angular/angular/commits/master))
``` ```
docs(changelog): update change log to beta.5 docs(changelog): update changelog to beta.5
``` ```
``` ```
fix(release): need to depend on latest rxjs and zone.js fix(release): need to depend on latest rxjs and zone.js
@ -203,7 +203,7 @@ Must be one of the following:
* **test**: Adding missing tests or correcting existing tests * **test**: Adding missing tests or correcting existing tests
### Scope ### Scope
The scope should be the name of the npm package affected (as perceived by person reading changelog generated from commit messages. The scope should be the name of the npm package affected (as perceived by the person reading the changelog generated from commit messages.
The following is the list of supported scopes: The following is the list of supported scopes:
@ -232,10 +232,10 @@ There are currently a few exceptions to the "use package name" rule:
* none/empty string: useful for `style`, `test` and `refactor` changes that are done across all packages (e.g. `style: add missing semicolons`) * none/empty string: useful for `style`, `test` and `refactor` changes that are done across all packages (e.g. `style: add missing semicolons`)
### Subject ### Subject
The subject contains succinct description of the change: The subject contains a succinct description of the change:
* use the imperative, present tense: "change" not "changed" nor "changes" * use the imperative, present tense: "change" not "changed" nor "changes"
* don't capitalize first letter * don't capitalize the first letter
* no dot (.) at the end * no dot (.) at the end
### Body ### Body
@ -259,6 +259,19 @@ changes to be accepted, the CLA must be signed. It's a quick process, we promise
* For corporations we'll need you to * For corporations we'll need you to
[print, sign and one of scan+email, fax or mail the form][corporate-cla]. [print, sign and one of scan+email, fax or mail the form][corporate-cla].
<hr>
If you have more than one Git identity, you must make sure that you sign the CLA using the primary email address associated with the ID that has been granted access to the Angular repository. Git identities can be associated with more than one email address, and only one is primary. Here are some links to help you sort out multiple Git identities and email addresses:
* https://help.github.com/articles/setting-your-commit-email-address-in-git/
* https://stackoverflow.com/questions/37245303/what-does-usera-committed-with-userb-13-days-ago-on-github-mean
* https://help.github.com/articles/about-commit-email-addresses/
* https://help.github.com/articles/blocking-command-line-pushes-that-expose-your-personal-email-address/
Note that if you have more than one Git identity, it is important to verify that you are logged in with the same ID with which you signed the CLA, before you commit changes. If not, your PR will fail the CLA check.
<hr>
[angular-group]: https://groups.google.com/forum/#!forum/angular [angular-group]: https://groups.google.com/forum/#!forum/angular
[coc]: https://github.com/angular/code-of-conduct/blob/master/CODE_OF_CONDUCT.md [coc]: https://github.com/angular/code-of-conduct/blob/master/CODE_OF_CONDUCT.md

View File

@ -1,11 +1,10 @@
workspace(name = "angular_src") workspace(name = "angular")
load("@bazel_tools//tools/build_defs/repo:git.bzl", "git_repository") http_archive(
git_repository(
name = "build_bazel_rules_nodejs", name = "build_bazel_rules_nodejs",
remote = "https://github.com/bazelbuild/rules_nodejs.git", url = "https://github.com/bazelbuild/rules_nodejs/archive/0.5.0.zip",
tag = "0.3.1", strip_prefix = "rules_nodejs-0.5.0",
sha256 = "06aabb253c3867d51724386ac5622a0a238bbd82e2c70ce1d09ee3ceac4c31d6",
) )
load("@build_bazel_rules_nodejs//:defs.bzl", "check_bazel_version", "node_repositories") load("@build_bazel_rules_nodejs//:defs.bzl", "check_bazel_version", "node_repositories")
@ -13,34 +12,32 @@ load("@build_bazel_rules_nodejs//:defs.bzl", "check_bazel_version", "node_reposi
check_bazel_version("0.9.0") check_bazel_version("0.9.0")
node_repositories(package_json = ["//:package.json"]) node_repositories(package_json = ["//:package.json"])
git_repository( http_archive(
name = "build_bazel_rules_typescript", name = "build_bazel_rules_typescript",
remote = "https://github.com/bazelbuild/rules_typescript.git", url = "https://github.com/bazelbuild/rules_typescript/archive/0.11.0.zip",
# tag = "0.7.1+", strip_prefix = "rules_typescript-0.11.0",
commit = "89d2c75066bea3d9c942f29dd1d2ea543c58d6d5" sha256 = "ce7bac7b5287d5162fcbe4f7c14ff507ae7d506ceb44626ad09f6b7e27d3260b",
) )
load("@build_bazel_rules_typescript//:setup.bzl", "ts_setup_workspace") load("@build_bazel_rules_typescript//:defs.bzl", "ts_setup_workspace")
ts_setup_workspace() ts_setup_workspace()
local_repository(
name = "angular",
path = "packages/bazel",
)
local_repository( local_repository(
name = "rxjs", name = "rxjs",
path = "node_modules/rxjs/src", path = "node_modules/rxjs/src",
) )
git_repository( # This commit matches the version of buildifier in angular/ngcontainer
# If you change this, also check if it matches the version in the angular/ngcontainer
# version in /.circleci/config.yml
BAZEL_BUILDTOOLS_VERSION = "b3b620e8bcff18ed3378cd3f35ebeb7016d71f71"
http_archive(
name = "com_github_bazelbuild_buildtools", name = "com_github_bazelbuild_buildtools",
remote = "https://github.com/bazelbuild/buildtools.git", url = "https://github.com/bazelbuild/buildtools/archive/%s.zip" % BAZEL_BUILDTOOLS_VERSION,
# Note, this commit matches the version of buildifier in angular/ngcontainer strip_prefix = "buildtools-%s" % BAZEL_BUILDTOOLS_VERSION,
# If you change this, also check if it matches the version in the angular/ngcontainer sha256 = "dad19224258ed67cbdbae9b7befb785c3b966e5a33b04b3ce58ddb7824b97d73",
# version in /.circleci/config.yml
commit = "b3b620e8bcff18ed3378cd3f35ebeb7016d71f71",
) )
http_archive( http_archive(

View File

@ -62,6 +62,9 @@
"styleExt": "scss", "styleExt": "scss",
"component": { "component": {
"inlineStyle": true "inlineStyle": true
},
"build": {
"namedChunks": true
} }
} }
} }

1
aio/.gitignore vendored
View File

@ -30,6 +30,7 @@
/connect.lock /connect.lock
/coverage /coverage
/libpeerconnection.log /libpeerconnection.log
debug.log
npm-debug.log npm-debug.log
testem.log testem.log
/typings /typings

View File

@ -4,7 +4,7 @@ Everything in this folder is part of the documentation project. This includes
* the web site for displaying the documentation * the web site for displaying the documentation
* the dgeni configuration for converting source files to rendered files that can be viewed in the web site. * the dgeni configuration for converting source files to rendered files that can be viewed in the web site.
* the tooling for setting up examples for development; and generating plunkers and zip files from the examples. * the tooling for setting up examples for development; and generating live-example and zip files from the examples.
## Developer tasks ## Developer tasks
@ -13,7 +13,7 @@ You should run all these tasks from the `angular/aio` folder.
Here are the most important tasks you might need to use: Here are the most important tasks you might need to use:
* `yarn` - install all the dependencies. * `yarn` - install all the dependencies.
* `yarn setup` - install all the dependencies, boilerplate, plunkers, zips and run dgeni on the docs. * `yarn setup` - install all the dependencies, boilerplate, stackblitz, zips and run dgeni on the docs.
* `yarn setup-local` - same as `setup`, but use the locally built Angular packages for aio and docs examples boilerplate. * `yarn setup-local` - same as `setup`, but use the locally built Angular packages for aio and docs examples boilerplate.
* `yarn build` - create a production build of the application (after installing dependencies, boilerplate, etc). * `yarn build` - create a production build of the application (after installing dependencies, boilerplate, etc).
@ -32,7 +32,7 @@ Here are the most important tasks you might need to use:
* `yarn boilerplate:add` - generate all the boilerplate code for the examples, so that they can be run locally. Add the option `--local` to use your local version of Angular contained in the "dist" folder. * `yarn boilerplate:add` - generate all the boilerplate code for the examples, so that they can be run locally. Add the option `--local` to use your local version of Angular contained in the "dist" folder.
* `yarn boilerplate:remove` - remove all the boilerplate code that was added via `yarn boilerplate:add`. * `yarn boilerplate:remove` - remove all the boilerplate code that was added via `yarn boilerplate:add`.
* `yarn generate-plunkers` - generate the plunker files that are used by the `live-example` tags in the docs. * `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 generate-zips` - generate the zip files from the examples. Zip available via the `live-example` tags in the docs.
* `yarn example-e2e` - run all e2e tests for examples * `yarn example-e2e` - run all e2e tests for examples
@ -68,6 +68,11 @@ The content is written in markdown.
All other content is written using markdown in text files, located in the `angular/aio/content` folder. All other content is written using markdown in text files, located in the `angular/aio/content` folder.
More specifically, there are sub-folders that contain particular types of content: guides, tutorial and marketing. More specifically, there are sub-folders that contain particular types of content: guides, tutorial and marketing.
* **Code examples**: code examples need to be testable to ensure their accuracy.
Also, our examples have a specific look and feel and allow the user to copy the source code. For larger
examples they are rendered in a tabbed interface (e.g. template, HTML, and TypeScript on separate
tabs). Additionally, some are live examples, which provide links where the code can be edited, executed, and/or downloaded. For details on working with code examples, please read the [Code snippets](https://angular.io/guide/docs-style-guide#code-snippets), [Source code markup](https://angular.io/guide/docs-style-guide#source-code-markup), and [Live examples](https://angular.io/guide/docs-style-guide#live-examples) pages of the [Authors Style Guide](https://angular.io/guide/docs-style-guide).
We use the [dgeni](https://github.com/angular/dgeni) tool to convert these files into docs that can be viewed in the doc-viewer. We use the [dgeni](https://github.com/angular/dgeni) tool to convert these files into docs that can be viewed in the doc-viewer.
The [Authors Style Guide](https://angular.io/guide/docs-style-guide) prescribes guidelines for The [Authors Style Guide](https://angular.io/guide/docs-style-guide) prescribes guidelines for
@ -100,8 +105,7 @@ The general setup is as follows:
* Open a terminal, ensure the dependencies are installed; run an initial doc generation; then start the doc-viewer: * Open a terminal, ensure the dependencies are installed; run an initial doc generation; then start the doc-viewer:
```bash ```bash
yarn yarn setup
yarn docs
yarn start yarn start
``` ```

View File

@ -8,52 +8,61 @@ LABEL name="angular.io PR preview" \
VOLUME /aio-secrets VOLUME /aio-secrets
VOLUME /var/www/aio-builds VOLUME /var/www/aio-builds
VOLUME /dockerbuild
EXPOSE 80 443 EXPOSE 80 443
# Build-time args and env vars # 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 AIO_BUILDS_DIR=/var/www/aio-builds
ARG TEST_AIO_BUILDS_DIR=/tmp/aio-builds ARG TEST_AIO_BUILDS_DIR=/tmp/aio-builds
ARG AIO_DOMAIN_NAME=ngbuilds.io ARG AIO_DOMAIN_NAME=ngbuilds.io
ARG TEST_AIO_DOMAIN_NAME=$AIO_DOMAIN_NAME.localhost ARG TEST_AIO_DOMAIN_NAME=$AIO_DOMAIN_NAME.localhost
ARG AIO_GITHUB_ORGANIZATION=angular ARG AIO_GITHUB_ORGANIZATION=angular
ARG TEST_AIO_GITHUB_ORGANIZATION=angular ARG TEST_AIO_GITHUB_ORGANIZATION=test-org
ARG AIO_GITHUB_TEAM_SLUGS=angular-core,aio-contributors ARG AIO_GITHUB_REPO=angular
ARG TEST_AIO_GITHUB_TEAM_SLUGS=angular-core,aio-contributors ARG TEST_AIO_GITHUB_REPO=test-repo
ARG AIO_GITHUB_TEAM_SLUGS=team,aio-contributors
ARG TEST_AIO_GITHUB_TEAM_SLUGS=team,aio-contributors
ARG AIO_NGINX_HOSTNAME=$AIO_DOMAIN_NAME ARG AIO_NGINX_HOSTNAME=$AIO_DOMAIN_NAME
ARG TEST_AIO_NGINX_HOSTNAME=$TEST_AIO_DOMAIN_NAME ARG TEST_AIO_NGINX_HOSTNAME=$TEST_AIO_DOMAIN_NAME
ARG AIO_NGINX_PORT_HTTP=80 ARG AIO_NGINX_PORT_HTTP=80
ARG TEST_AIO_NGINX_PORT_HTTP=8080 ARG TEST_AIO_NGINX_PORT_HTTP=8080
ARG AIO_NGINX_PORT_HTTPS=443 ARG AIO_NGINX_PORT_HTTPS=443
ARG TEST_AIO_NGINX_PORT_HTTPS=4433 ARG TEST_AIO_NGINX_PORT_HTTPS=4433
ARG AIO_REPO_SLUG=angular/angular ARG AIO_SIGNIFICANT_FILES_PATTERN='^(?:aio|packages)/(?!.*[._]spec\\.[jt]s$)'
ARG TEST_AIO_REPO_SLUG=test-repo/test-slug ARG TEST_AIO_SIGNIFICANT_FILES_PATTERN=$AIO_SIGNIFICANT_FILES_PATTERN
ARG AIO_TRUSTED_PR_LABEL="aio: preview" ARG AIO_TRUSTED_PR_LABEL="aio: preview"
ARG TEST_AIO_TRUSTED_PR_LABEL="aio: preview" ARG TEST_AIO_TRUSTED_PR_LABEL="aio: preview"
ARG AIO_UPLOAD_HOSTNAME=upload.localhost ARG AIO_PREVIEW_SERVER_HOSTNAME=preview.localhost
ARG TEST_AIO_UPLOAD_HOSTNAME=upload.localhost ARG TEST_AIO_PREVIEW_SERVER_HOSTNAME=preview.localhost
ARG AIO_UPLOAD_MAX_SIZE=20971520 ARG AIO_ARTIFACT_MAX_SIZE=20971520
ARG TEST_AIO_UPLOAD_MAX_SIZE=20971520 ARG TEST_AIO_ARTIFACT_MAX_SIZE=200
ARG AIO_UPLOAD_PORT=3000 ARG AIO_PREVIEW_SERVER_PORT=3000
ARG TEST_AIO_UPLOAD_PORT=3001 ARG TEST_AIO_PREVIEW_SERVER_PORT=3001
ENV AIO_BUILDS_DIR=$AIO_BUILDS_DIR TEST_AIO_BUILDS_DIR=$TEST_AIO_BUILDS_DIR \ 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_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_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_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_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_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_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_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_NGINX_PORT_HTTPS=$AIO_NGINX_PORT_HTTPS TEST_AIO_NGINX_PORT_HTTPS=$TEST_AIO_NGINX_PORT_HTTPS \
AIO_REPO_SLUG=$AIO_REPO_SLUG TEST_AIO_REPO_SLUG=$TEST_AIO_REPO_SLUG \
AIO_SCRIPTS_JS_DIR=/usr/share/aio-scripts-js \ AIO_SCRIPTS_JS_DIR=/usr/share/aio-scripts-js \
AIO_SCRIPTS_SH_DIR=/usr/share/aio-scripts-sh \ 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_TRUSTED_PR_LABEL=$AIO_TRUSTED_PR_LABEL TEST_AIO_TRUSTED_PR_LABEL=$TEST_AIO_TRUSTED_PR_LABEL \
AIO_UPLOAD_HOSTNAME=$AIO_UPLOAD_HOSTNAME TEST_AIO_UPLOAD_HOSTNAME=$TEST_AIO_UPLOAD_HOSTNAME \ AIO_PREVIEW_SERVER_HOSTNAME=$AIO_PREVIEW_SERVER_HOSTNAME TEST_AIO_PREVIEW_SERVER_HOSTNAME=$TEST_AIO_PREVIEW_SERVER_HOSTNAME \
AIO_UPLOAD_MAX_SIZE=$AIO_UPLOAD_MAX_SIZE TEST_AIO_UPLOAD_MAX_SIZE=$TEST_AIO_UPLOAD_MAX_SIZE \ AIO_ARTIFACT_MAX_SIZE=$AIO_ARTIFACT_MAX_SIZE TEST_AIO_ARTIFACT_MAX_SIZE=$TEST_AIO_ARTIFACT_MAX_SIZE \
AIO_UPLOAD_PORT=$AIO_UPLOAD_PORT TEST_AIO_UPLOAD_PORT=$TEST_AIO_UPLOAD_PORT \ AIO_PREVIEW_SERVER_PORT=$AIO_PREVIEW_SERVER_PORT TEST_AIO_PREVIEW_SERVER_PORT=$TEST_AIO_PREVIEW_SERVER_PORT \
AIO_WWW_USER=www-data \ AIO_WWW_USER=www-data \
NODE_ENV=production NODE_ENV=production
@ -64,7 +73,7 @@ RUN mkdir /var/log/aio
# Add extra package sources # Add extra package sources
RUN apt-get update -y && apt-get install -y curl RUN apt-get update -y && apt-get install -y curl
RUN curl --silent --show-error --location https://deb.nodesource.com/setup_6.x | bash - RUN curl --silent --show-error --location https://deb.nodesource.com/setup_10.x | bash -
RUN curl --silent --show-error https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - 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 RUN echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list
RUN echo "deb http://ftp.debian.org/debian jessie-backports main" | tee /etc/apt/sources.list.d/backports.list RUN echo "deb http://ftp.debian.org/debian jessie-backports main" | tee /etc/apt/sources.list.d/backports.list
@ -99,9 +108,9 @@ RUN printenv | grep AIO_ >> /etc/environment
# Set up dnsmasq # Set up dnsmasq
COPY dnsmasq/dnsmasq.conf /etc/ COPY dnsmasq/dnsmasq.conf /etc/
RUN sed -i "s|{{\$AIO_NGINX_HOSTNAME}}|$AIO_NGINX_HOSTNAME|g" /etc/dnsmasq.conf RUN sed -i "s|{{\$AIO_NGINX_HOSTNAME}}|$AIO_NGINX_HOSTNAME|g" /etc/dnsmasq.conf
RUN sed -i "s|{{\$AIO_UPLOAD_HOSTNAME}}|$AIO_UPLOAD_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_NGINX_HOSTNAME}}|$TEST_AIO_NGINX_HOSTNAME|g" /etc/dnsmasq.conf
RUN sed -i "s|{{\$TEST_AIO_UPLOAD_HOSTNAME}}|$TEST_AIO_UPLOAD_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 # Set up SSL/TLS certificates
@ -125,9 +134,9 @@ RUN sed -i "s|{{\$AIO_LOCALCERTS_DIR}}|$AIO_LOCALCERTS_DIR|g" /etc/nginx/conf.d/
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_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_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_NGINX_PORT_HTTPS}}|$AIO_NGINX_PORT_HTTPS|g" /etc/nginx/conf.d/aio-builds-prod.conf
RUN sed -i "s|{{\$AIO_UPLOAD_HOSTNAME}}|$AIO_UPLOAD_HOSTNAME|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_UPLOAD_MAX_SIZE}}|$AIO_UPLOAD_MAX_SIZE|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_UPLOAD_PORT}}|$AIO_UPLOAD_PORT|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 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_BUILDS_DIR}}|$TEST_AIO_BUILDS_DIR|g" /etc/nginx/conf.d/aio-builds-test.conf
@ -136,9 +145,9 @@ RUN sed -i "s|{{\$AIO_LOCALCERTS_DIR}}|$TEST_AIO_LOCALCERTS_DIR|g" /etc/nginx/co
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_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_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_NGINX_PORT_HTTPS}}|$TEST_AIO_NGINX_PORT_HTTPS|g" /etc/nginx/conf.d/aio-builds-test.conf
RUN sed -i "s|{{\$AIO_UPLOAD_HOSTNAME}}|$TEST_AIO_UPLOAD_HOSTNAME|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_UPLOAD_MAX_SIZE}}|$TEST_AIO_UPLOAD_MAX_SIZE|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_UPLOAD_PORT}}|$TEST_AIO_UPLOAD_PORT|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 # Set up pm2

View File

@ -8,9 +8,9 @@ listen-address=127.0.0.1
# Force an IP address for these domains. # Force an IP address for these domains.
address=/{{$AIO_NGINX_HOSTNAME}}/127.0.0.1 address=/{{$AIO_NGINX_HOSTNAME}}/127.0.0.1
address=/{{$AIO_UPLOAD_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_NGINX_HOSTNAME}}/127.0.0.1
address=/{{$TEST_AIO_UPLOAD_HOSTNAME}}/127.0.0.1 address=/{{$TEST_AIO_PREVIEW_SERVER_HOSTNAME}}/127.0.0.1
# Run as root (required from inside docker container). # Run as root (required from inside docker container).
user=root user=root

View File

@ -1,4 +1,4 @@
/var/log/aio/upload-server-*.log { /var/log/aio/preview-server-*.log {
compress compress
copytruncate copytruncate
delaycompress delaycompress

View File

@ -66,24 +66,17 @@ server {
return 200 ''; return 200 '';
} }
# Upload builds # Notify about CircleCI builds
location "~^/create-build/(?<pr>[1-9][0-9]*)/(?<sha>[0-9a-f]{40})/?$" { location "~^/circle-build/?$" {
if ($request_method != "POST") { if ($request_method != "POST") {
add_header Allow "POST"; add_header Allow "POST";
return 405; return 405;
} }
client_body_temp_path /tmp/aio-create-builds;
client_body_buffer_size 128K;
client_max_body_size {{$AIO_UPLOAD_MAX_SIZE}};
client_body_in_file_only on;
proxy_pass_request_headers on; proxy_pass_request_headers on;
proxy_set_header X-FILE $request_body_file;
proxy_set_body off;
proxy_redirect off; proxy_redirect off;
proxy_method GET; proxy_method POST;
proxy_pass http://{{$AIO_UPLOAD_HOSTNAME}}:{{$AIO_UPLOAD_PORT}}$request_uri; proxy_pass http://{{$AIO_PREVIEW_SERVER_HOSTNAME}}:{{$AIO_PREVIEW_SERVER_PORT}}$request_uri;
resolver 127.0.0.1; resolver 127.0.0.1;
} }
@ -98,7 +91,7 @@ server {
proxy_pass_request_headers on; proxy_pass_request_headers on;
proxy_redirect off; proxy_redirect off;
proxy_method POST; proxy_method POST;
proxy_pass http://{{$AIO_UPLOAD_HOSTNAME}}:{{$AIO_UPLOAD_PORT}}$request_uri; proxy_pass http://{{$AIO_PREVIEW_SERVER_HOSTNAME}}:{{$AIO_PREVIEW_SERVER_PORT}}$request_uri;
resolver 127.0.0.1; resolver 127.0.0.1;
} }

View File

@ -3,29 +3,53 @@ import * as fs from 'fs';
import * as path from 'path'; import * as path from 'path';
import * as shell from 'shelljs'; import * as shell from 'shelljs';
import {HIDDEN_DIR_PREFIX} from '../common/constants'; import {HIDDEN_DIR_PREFIX} from '../common/constants';
import {GithubApi} from '../common/github-api';
import {GithubPullRequests} from '../common/github-pull-requests'; import {GithubPullRequests} from '../common/github-pull-requests';
import {assertNotMissingOrEmpty} from '../common/utils'; import {assertNotMissingOrEmpty, createLogger, getPrInfoFromDownloadPath} from '../common/utils';
// Classes // Classes
export class BuildCleaner { export class BuildCleaner {
private logger = createLogger('BuildCleaner');
// Constructor // Constructor
constructor(protected buildsDir: string, protected repoSlug: string, protected githubToken: string) { constructor(protected buildsDir: string, protected githubOrg: string, protected githubRepo: string,
protected githubToken: string, protected downloadsDir: string, protected artifactPath: string) {
assertNotMissingOrEmpty('buildsDir', buildsDir); assertNotMissingOrEmpty('buildsDir', buildsDir);
assertNotMissingOrEmpty('repoSlug', repoSlug); assertNotMissingOrEmpty('githubOrg', githubOrg);
assertNotMissingOrEmpty('githubRepo', githubRepo);
assertNotMissingOrEmpty('githubToken', githubToken); assertNotMissingOrEmpty('githubToken', githubToken);
assertNotMissingOrEmpty('downloadsDir', downloadsDir);
assertNotMissingOrEmpty('artifactPath', artifactPath);
} }
// Methods - Public // Methods - Public
public cleanUp(): Promise<void> { public async cleanUp(): Promise<void> {
return Promise.all([ try {
this.getExistingBuildNumbers(), this.logger.log('Cleaning up builds and downloads');
this.getOpenPrNumbers(), const openPrs = await this.getOpenPrNumbers();
]).then(([existingBuilds, openPrs]) => this.removeUnnecessaryBuilds(existingBuilds, openPrs)); this.logger.log(`Open pull requests: ${openPrs.length}`);
await Promise.all([
this.cleanBuilds(openPrs),
this.cleanDownloads(openPrs),
]);
} catch (error) {
this.logger.error('ERROR:', error);
}
} }
// Methods - Protected public async cleanBuilds(openPrs: number[]): Promise<void> {
protected getExistingBuildNumbers(): Promise<number[]> { const existingBuilds = await this.getExistingBuildNumbers();
return new Promise((resolve, reject) => { 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) => { fs.readdir(this.buildsDir, (err, files) => {
if (err) { if (err) {
return reject(err); return reject(err);
@ -41,15 +65,14 @@ export class BuildCleaner {
}); });
} }
protected getOpenPrNumbers(): Promise<number[]> { public async getOpenPrNumbers(): Promise<number[]> {
const githubPullRequests = new GithubPullRequests(this.githubToken, this.repoSlug); const api = new GithubApi(this.githubToken);
const githubPullRequests = new GithubPullRequests(api, this.githubOrg, this.githubRepo);
return githubPullRequests. const prs = await githubPullRequests.fetchAll('open');
fetchAll('open'). return prs.map(pr => pr.number);
then(prs => prs.map(pr => pr.number));
} }
protected removeDir(dir: string) { public removeDir(dir: string): void {
try { try {
if (shell.test('-d', dir)) { if (shell.test('-d', dir)) {
// Undocumented signature (see https://github.com/shelljs/shelljs/pull/663). // Undocumented signature (see https://github.com/shelljs/shelljs/pull/663).
@ -57,16 +80,15 @@ export class BuildCleaner {
shell.rm('-rf', dir); shell.rm('-rf', dir);
} }
} catch (err) { } catch (err) {
console.error(`ERROR: Unable to remove '${dir}' due to:`, err); this.logger.error(`ERROR: Unable to remove '${dir}' due to:`, err);
} }
} }
protected removeUnnecessaryBuilds(existingBuildNumbers: number[], openPrNumbers: number[]) { public removeUnnecessaryBuilds(existingBuildNumbers: number[], openPrNumbers: number[]): void {
const toRemove = existingBuildNumbers.filter(num => !openPrNumbers.includes(num)); const toRemove = existingBuildNumbers.filter(num => !openPrNumbers.includes(num));
console.log(`Existing builds: ${existingBuildNumbers.length}`); this.logger.log(`Existing builds: ${existingBuildNumbers.length}`);
console.log(`Open pull requests: ${openPrNumbers.length}`); this.logger.log(`Removing ${toRemove.length} build(s): ${toRemove.join(', ')}`);
console.log(`Removing ${toRemove.length} build(s): ${toRemove.join(', ')}`);
// Try removing public dirs. // Try removing public dirs.
toRemove. toRemove.
@ -78,4 +100,29 @@ export class BuildCleaner {
map(num => path.join(this.buildsDir, HIDDEN_DIR_PREFIX + String(num))). map(num => path.join(this.buildsDir, HIDDEN_DIR_PREFIX + String(num))).
forEach(dir => this.removeDir(dir)); 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(filePath));
}
} }

View File

@ -1,23 +1,26 @@
// Imports // Imports
import {getEnvVar} from '../common/utils'; 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'; import {BuildCleaner} from './build-cleaner';
// Constants
const AIO_BUILDS_DIR = getEnvVar('AIO_BUILDS_DIR');
const AIO_GITHUB_TOKEN = getEnvVar('AIO_GITHUB_TOKEN', true);
const AIO_REPO_SLUG = getEnvVar('AIO_REPO_SLUG');
// Run // Run
_main(); _main();
// Functions // Functions
function _main() { function _main(): void {
console.log(`[${new Date()}] - Cleaning up builds...`); const buildCleaner = new BuildCleaner(
AIO_BUILDS_DIR,
AIO_GITHUB_ORGANIZATION,
AIO_GITHUB_REPO,
AIO_GITHUB_TOKEN,
AIO_DOWNLOADS_DIR,
AIO_ARTIFACT_PATH);
const buildCleaner = new BuildCleaner(AIO_BUILDS_DIR, AIO_REPO_SLUG, AIO_GITHUB_TOKEN); buildCleaner.cleanUp().catch(() => process.exit(1));
buildCleaner.cleanUp().catch(err => {
console.error('ERROR:', err);
process.exit(1);
});
} }

View File

@ -0,0 +1,90 @@
// Imports
import fetch from 'node-fetch';
import {assertNotMissingOrEmpty} from './utils';
// Constants
const CIRCLE_CI_API_URL = 'https://circleci.com/api/v1.1/project/github';
// Interfaces - Types
export interface ArtifactInfo {
path: string;
pretty_path: string;
node_index: number;
url: string;
}
export type ArtifactResponse = ArtifactInfo[];
export interface BuildInfo {
reponame: string;
failed: boolean;
branch: string;
username: string;
build_num: number;
has_artifacts: boolean;
outcome: string; // e.g. 'success'
vcs_revision: string; // HEAD SHA
// there are other fields but they are not used in this code
}
/**
* A Helper that can interact with the CircleCI API.
*/
export class CircleCiApi {
private tokenParam = `circle-token=${this.circleCiToken}`;
/**
* 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);
}
/**
* Get the info for a build 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 baseUrl = `${CIRCLE_CI_API_URL}/${this.githubOrg}/${this.githubRepo}/${buildNumber}`;
const response = await fetch(`${baseUrl}?${this.tokenParam}`);
if (response.status !== 200) {
throw new Error(`${baseUrl}: ${response.status} - ${response.statusText}`);
}
return response.json<BuildInfo>();
} catch (error) {
throw new Error(`CircleCI build info request failed (${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_API_URL}/${this.githubOrg}/${this.githubRepo}/${buildNumber}`;
try {
const response = await fetch(`${baseUrl}/artifacts?${this.tokenParam}`);
const artifacts = await response.json<ArtifactResponse>();
const artifact = artifacts.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.message})`);
}
}
}

View File

@ -1,3 +1,4 @@
// Constants // Constants
export const AIO_DOWNLOADS_DIR = '/tmp/aio-downloads';
export const HIDDEN_DIR_PREFIX = 'hidden--'; export const HIDDEN_DIR_PREFIX = 'hidden--';
export const SHORT_SHA_LEN = 7; export const SHORT_SHA_LEN = 7;

View File

@ -0,0 +1,19 @@
import {getEnvVar} from './utils';
export const AIO_ARTIFACT_PATH = getEnvVar('AIO_ARTIFACT_PATH');
export const AIO_BUILDS_DIR = getEnvVar('AIO_BUILDS_DIR');
export const AIO_GITHUB_TOKEN = getEnvVar('AIO_GITHUB_TOKEN');
export const AIO_CIRCLE_CI_TOKEN = getEnvVar('AIO_CIRCLE_CI_TOKEN');
export const AIO_DOMAIN_NAME = getEnvVar('AIO_DOMAIN_NAME');
export const AIO_GITHUB_ORGANIZATION = getEnvVar('AIO_GITHUB_ORGANIZATION');
export const AIO_GITHUB_REPO = getEnvVar('AIO_GITHUB_REPO');
export const AIO_GITHUB_TEAM_SLUGS = getEnvVar('AIO_GITHUB_TEAM_SLUGS');
export const AIO_NGINX_HOSTNAME = getEnvVar('AIO_NGINX_HOSTNAME');
export const AIO_NGINX_PORT_HTTP = +getEnvVar('AIO_NGINX_PORT_HTTP');
export const AIO_NGINX_PORT_HTTPS = +getEnvVar('AIO_NGINX_PORT_HTTPS');
export const AIO_SIGNIFICANT_FILES_PATTERN = getEnvVar('AIO_SIGNIFICANT_FILES_PATTERN');
export const AIO_TRUSTED_PR_LABEL = getEnvVar('AIO_TRUSTED_PR_LABEL');
export const AIO_PREVIEW_SERVER_HOSTNAME = getEnvVar('AIO_PREVIEW_SERVER_HOSTNAME');
export const AIO_PREVIEW_SERVER_PORT = +getEnvVar('AIO_PREVIEW_SERVER_PORT');
export const AIO_ARTIFACT_MAX_SIZE = +getEnvVar('AIO_ARTIFACT_MAX_SIZE');
export const AIO_WWW_USER = getEnvVar('AIO_WWW_USER');

View File

@ -28,29 +28,17 @@ export class GithubApi {
} }
// Methods - Public // Methods - Public
public get<T>(pathname: string, params?: RequestParamsOrNull): Promise<T> { public get<T = any>(pathname: string, params?: RequestParamsOrNull): Promise<T> {
const path = this.buildPath(pathname, params); const path = this.buildPath(pathname, params);
return this.request<T>('get', path); return this.request<T>('get', path);
} }
public post<T>(pathname: string, params?: RequestParamsOrNull, data?: any): Promise<T> { public post<T = any>(pathname: string, params?: RequestParamsOrNull, data?: any): Promise<T> {
const path = this.buildPath(pathname, params); const path = this.buildPath(pathname, params);
return this.request<T>('post', path, data); return this.request<T>('post', path, data);
} }
// Methods - Protected public getPaginated<T>(pathname: string, baseParams: RequestParams = {}, currentPage: number = 0): Promise<T[]> {
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 getPaginated<T>(pathname: string, baseParams: RequestParams = {}, currentPage: number = 0): Promise<T[]> {
const perPage = 100; const perPage = 100;
const params = { const params = {
...baseParams, ...baseParams,
@ -67,6 +55,18 @@ export class GithubApi {
}); });
} }
// 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> { protected request<T>(method: string, path: string, data: any = null): Promise<T> {
return new Promise<T>((resolve, reject) => { return new Promise<T>((resolve, reject) => {
const options = { const options = {
@ -81,7 +81,7 @@ export class GithubApi {
reject(`Request to '${url}' failed (status: ${statusCode}): ${responseText}`); reject(`Request to '${url}' failed (status: ${statusCode}): ${responseText}`);
}; };
const onSuccess = (responseText: string) => { const onSuccess = (responseText: string) => {
try { resolve(JSON.parse(responseText)); } catch (err) { reject(err); } try { resolve(responseText && JSON.parse(responseText)); } catch (err) { reject(err); }
}; };
const onResponse = (res: IncomingMessage) => { const onResponse = (res: IncomingMessage) => {
const statusCode = res.statusCode || -1; const statusCode = res.statusCode || -1;

View File

@ -1,46 +1,79 @@
// Imports
import {assertNotMissingOrEmpty} from '../common/utils';
import {GithubApi} from './github-api'; import {GithubApi} from './github-api';
import {assert, assertNotMissingOrEmpty} from './utils';
// Interfaces - Types
export interface PullRequest { export interface PullRequest {
number: number; number: number;
user: {login: string}; user: {login: string};
labels: {name: string}[]; labels: {name: string}[];
} }
export interface FileInfo {
sha: string;
filename: string;
}
export type PullRequestState = 'all' | 'closed' | 'open'; export type PullRequestState = 'all' | 'closed' | 'open';
// Classes /**
export class GithubPullRequests extends GithubApi { * Access pull requests on GitHub.
// Constructor */
constructor(githubToken: string, protected repoSlug: string) { export class GithubPullRequests {
super(githubToken); public repoSlug: string;
assertNotMissingOrEmpty('repoSlug', repoSlug);
/**
* 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}`;
} }
// Methods - Public /**
public addComment(pr: number, body: string): Promise<void> { * Post a comment on a PR.
if (!(pr > 0)) { * @param pr The number of the PR on which to comment.
throw new Error(`Invalid PR number: ${pr}`); * @param body The body of the comment to post.
} else if (!body) { * @returns A promise that resolves when the comment has been posted.
throw new Error(`Invalid or empty comment body: ${body}`); */
} public addComment(pr: number, body: string): Promise<any> {
assert(pr > 0, `Invalid PR number: ${pr}`);
return this.post<void>(`/repos/${this.repoSlug}/issues/${pr}/comments`, null, {body}); 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> { 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. // Using the `/issues/` URL, because the `/pulls/` one does not provide labels.
return this.get<PullRequest>(`/repos/${this.repoSlug}/issues/${pr}`); 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[]> { public fetchAll(state: PullRequestState = 'all'): Promise<PullRequest[]> {
console.log(`Fetching ${state} pull requests...`);
const pathname = `/repos/${this.repoSlug}/pulls`; const pathname = `/repos/${this.repoSlug}/pulls`;
const params = {state}; const params = {state};
return this.getPaginated<PullRequest>(pathname, params); 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.get<FileInfo[]>(`/repos/${this.repoSlug}/pulls/${pr}/files`);
} }
} }

View File

@ -1,45 +1,72 @@
// Imports
import {assertNotMissingOrEmpty} from '../common/utils';
import {GithubApi} from './github-api'; import {GithubApi} from './github-api';
import {assertNotMissingOrEmpty} from './utils';
// Interfaces - Types export interface Team {
interface Team {
id: number; id: number;
slug: string; slug: string;
} }
interface TeamMembership { export interface TeamMembership {
state: string; state: string;
} }
// Classes export class GithubTeams {
export class GithubTeams extends GithubApi { /**
// Constructor * Create an instance of this helper
constructor(githubToken: string, protected organization: string) { * @param api An instance of the Github API helper.
super(githubToken); * @param githubOrg The organisation on GitHub whose repo we will interrogate.
assertNotMissingOrEmpty('organization', organization); */
constructor(private api: GithubApi, protected githubOrg: string) {
assertNotMissingOrEmpty('githubOrg', githubOrg);
} }
// Methods - Public /**
* 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[]> { public fetchAll(): Promise<Team[]> {
return this.getPaginated<Team>(`/orgs/${this.organization}/teams`); return this.api.getPaginated<Team>(`/orgs/${this.githubOrg}/teams`);
} }
public isMemberById(username: string, teamIds: number[]): Promise<boolean> { /**
const getMembership = (teamId: number) => * Check whether the specified username is a member of the specified team.
this.get<TeamMembership>(`/teams/${teamId}/memberships/${username}`). * @param username The usernane to check for in the team.
then(membership => membership.state === 'active'). * @param teamIds The team to check for the username.
catch(() => false); * @returns a Promise that resolves to `true` if the username is a member of the team.
const reduceFn = (promise: Promise<boolean>, teamId: number) => */
promise.then(isMember => isMember || getMembership(teamId)); public async isMemberById(username: string, teamIds: number[]): Promise<boolean> {
return teamIds.reduce(reduceFn, Promise.resolve(false)); 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;
}
} }
public isMemberBySlug(username: string, teamSlugs: string[]): Promise<boolean> { return false;
return this.fetchAll(). }
then(teams => teams.filter(team => teamSlugs.includes(team.slug)).map(team => team.id)).
then(teamIds => this.isMemberById(username, teamIds)). /**
catch(() => 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 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;
}
} }
} }

View File

@ -1,17 +1,85 @@
// Functions import {basename, resolve as resolvePath} from 'path';
export const assertNotMissingOrEmpty = (name: string, value: string | null | undefined) => { 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.substr(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) { if (!value) {
throw new Error(`Missing or empty required parameter '${name}'!`); 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 => { export const getEnvVar = (name: string, isOptional = false): string => {
const value = process.env[name]; const value = process.env[name];
if (!isOptional && !value) { if (!isOptional && !value) {
console.error(`ERROR: Missing required environment variable '${name}'!`); try {
throw new Error(`ERROR: Missing required environment variable '${name}'!`);
} catch (error) {
console.error(error.stack);
process.exit(1); process.exit(1);
} }
}
return value || ''; return value || '';
}; };
export function createLogger(scope: string) {
const padding = ' '.repeat(20 - scope.length);
return {
error: (...args: any[]) => console.error(`[${new Date()}]`, `${scope}:${padding}`, ...args),
info: (...args: any[]) => console.info(`[${new Date()}]`, `${scope}:${padding}`, ...args),
log: (...args: any[]) => console.log(`[${new Date()}]`, `${scope}:${padding}`, ...args),
warn: (...args: any[]) => console.warn(`[${new Date()}]`, `${scope}:${padding}`, ...args),
};
}

View File

@ -4,13 +4,16 @@ import {EventEmitter} from 'events';
import * as fs from 'fs'; import * as fs from 'fs';
import * as path from 'path'; import * as path from 'path';
import * as shell from 'shelljs'; import * as shell from 'shelljs';
import {HIDDEN_DIR_PREFIX, SHORT_SHA_LEN} from '../common/constants'; import {HIDDEN_DIR_PREFIX} from '../common/constants';
import {assertNotMissingOrEmpty} from '../common/utils'; import {assertNotMissingOrEmpty, computeShortSha, createLogger} from '../common/utils';
import {ChangedPrVisibilityEvent, CreatedBuildEvent} from './build-events'; import {ChangedPrVisibilityEvent, CreatedBuildEvent} from './build-events';
import {UploadError} from './upload-error'; import {PreviewServerError} from './preview-error';
// Classes // Classes
export class BuildCreator extends EventEmitter { export class BuildCreator extends EventEmitter {
private logger = createLogger('BuildCreator');
// Constructor // Constructor
constructor(protected buildsDir: string) { constructor(protected buildsDir: string) {
super(); super();
@ -18,9 +21,9 @@ export class BuildCreator extends EventEmitter {
} }
// Methods - Public // Methods - Public
public create(pr: string, sha: string, archivePath: string, isPublic: boolean): Promise<void> { public create(pr: number, sha: string, archivePath: string, isPublic: boolean): Promise<void> {
// Use only part of the SHA for more readable URLs. // Use only part of the SHA for more readable URLs.
sha = sha.substr(0, SHORT_SHA_LEN); sha = computeShortSha(sha);
const {newPrDir: prDir} = this.getCandidatePrDirs(pr, isPublic); const {newPrDir: prDir} = this.getCandidatePrDirs(pr, isPublic);
const shaDir = path.join(prDir, sha); const shaDir = path.join(prDir, sha);
@ -33,7 +36,7 @@ export class BuildCreator extends EventEmitter {
then(([prDirExisted, shaDirExisted]) => { then(([prDirExisted, shaDirExisted]) => {
if (shaDirExisted) { if (shaDirExisted) {
const publicOrNot = isPublic ? 'public' : 'non-public'; const publicOrNot = isPublic ? 'public' : 'non-public';
throw new UploadError(409, `Request to overwrite existing ${publicOrNot} directory: ${shaDir}`); throw new PreviewServerError(409, `Request to overwrite existing ${publicOrNot} directory: ${shaDir}`);
} }
dirToRemoveOnError = prDirExisted ? shaDir : prDir; dirToRemoveOnError = prDirExisted ? shaDir : prDir;
@ -49,15 +52,15 @@ export class BuildCreator extends EventEmitter {
shell.rm('-rf', dirToRemoveOnError); shell.rm('-rf', dirToRemoveOnError);
} }
if (!(err instanceof UploadError)) { if (!(err instanceof PreviewServerError)) {
err = new UploadError(500, `Error while uploading to directory: ${shaDir}\n${err}`); err = new PreviewServerError(500, `Error while creating preview at: ${shaDir}\n${err}`);
} }
throw err; throw err;
}); });
} }
public updatePrVisibility(pr: string, makePublic: boolean): Promise<boolean> { public updatePrVisibility(pr: number, makePublic: boolean): Promise<boolean> {
const {oldPrDir: otherVisPrDir, newPrDir: targetVisPrDir} = this.getCandidatePrDirs(pr, makePublic); const {oldPrDir: otherVisPrDir, newPrDir: targetVisPrDir} = this.getCandidatePrDirs(pr, makePublic);
return Promise. return Promise.
@ -68,7 +71,8 @@ export class BuildCreator extends EventEmitter {
return false; return false;
} else if (targetVisPrDirExisted) { } else if (targetVisPrDirExisted) {
// Error: Directories for both visibilities exist. // Error: Directories for both visibilities exist.
throw new UploadError(409, `Request to move '${otherVisPrDir}' to existing directory '${targetVisPrDir}'.`); throw new PreviewServerError(409,
`Request to move '${otherVisPrDir}' to existing directory '${targetVisPrDir}'.`);
} }
// Visibility change: Moving `otherVisPrDir` to `targetVisPrDir`. // Visibility change: Moving `otherVisPrDir` to `targetVisPrDir`.
@ -79,8 +83,8 @@ export class BuildCreator extends EventEmitter {
then(() => true); then(() => true);
}). }).
catch(err => { catch(err => {
if (!(err instanceof UploadError)) { if (!(err instanceof PreviewServerError)) {
err = new UploadError(500, `Error while making PR ${pr} ${makePublic ? 'public' : 'hidden'}.\n${err}`); err = new PreviewServerError(500, `Error while making PR ${pr} ${makePublic ? 'public' : 'hidden'}.\n${err}`);
} }
throw err; throw err;
@ -102,7 +106,7 @@ export class BuildCreator extends EventEmitter {
} }
if (stderr) { if (stderr) {
console.warn(stderr); this.logger.warn(stderr);
} }
try { try {
@ -117,9 +121,9 @@ export class BuildCreator extends EventEmitter {
}); });
} }
protected getCandidatePrDirs(pr: string, isPublic: boolean) { protected getCandidatePrDirs(pr: number, isPublic: boolean): {oldPrDir: string, newPrDir: string} {
const hiddenPrDir = path.join(this.buildsDir, HIDDEN_DIR_PREFIX + pr); const hiddenPrDir = path.join(this.buildsDir, HIDDEN_DIR_PREFIX + pr);
const publicPrDir = path.join(this.buildsDir, pr); const publicPrDir = path.join(this.buildsDir, `${pr}`);
const oldPrDir = isPublic ? hiddenPrDir : publicPrDir; const oldPrDir = isPublic ? hiddenPrDir : publicPrDir;
const newPrDir = isPublic ? publicPrDir : hiddenPrDir; const newPrDir = isPublic ? publicPrDir : hiddenPrDir;

View File

@ -0,0 +1,83 @@
import * as fs from 'fs';
import fetch from 'node-fetch';
import {dirname} from 'path';
import {mkdir} from 'shelljs';
import {promisify} from 'util';
import {CircleCiApi} from '../common/circle-ci-api';
import {assert, assertNotMissingOrEmpty, computeArtifactDownloadPath, createLogger} from '../common/utils';
import {PreviewServerError} from './preview-error';
export interface GithubInfo {
org: string;
pr: number;
repo: string;
sha: string;
success: boolean;
}
/**
* A helper that can get information about builds and download build artifacts.
*/
export class BuildRetriever {
private logger = createLogger('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 githubInfo: GithubInfo = {
org: buildInfo.username,
pr: getPrfromBranch(buildInfo.branch),
repo: buildInfo.reponame,
sha: buildInfo.vcs_revision,
success: !buildInfo.failed,
};
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 fetch(url, {size: this.downloadSizeLimit});
if (response.status !== 200) {
throw new PreviewServerError(response.status, `Error ${response.status} - ${response.statusText}`);
}
const buffer = await response.buffer();
mkdir('-p', dirname(outPath));
await promisify(fs.writeFile)(outPath, buffer);
}
return outPath;
} catch (error) {
this.logger.warn(error);
const status = (error.type === 'max-size') ? 413 : 500;
throw new PreviewServerError(status, `CircleCI artifact download failed (${error.message || error})`);
}
}
}
function getPrfromBranch(branch: string): number {
// CircleCI only exposes PR numbers via the `branch` field :-(
const match = /^pull\/(\d+)$/.exec(branch);
if (!match) {
throw new Error(`No PR found in branch field: ${branch}`);
}
return +match[1];
}

View File

@ -0,0 +1,46 @@
import {GithubPullRequests, PullRequest} from '../common/github-pull-requests';
import {GithubTeams} from '../common/github-teams';
import {assertNotMissingOrEmpty} from '../common/utils';
/**
* A helper to verify whether builds are trusted.
*/
export class BuildVerifier {
/**
* Construct a new BuildVerifier instance.
* @param prs A helper to access PR information.
* @param teams A helper to access Github team information.
* @param allowedTeamSlugs The teams that are trusted.
* @param trustedPrLabel The github label that indicates that a PR is trusted.
*/
constructor(protected prs: GithubPullRequests, protected teams: GithubTeams,
protected allowedTeamSlugs: string[], protected trustedPrLabel: string) {
assertNotMissingOrEmpty('allowedTeamSlugs', allowedTeamSlugs && allowedTeamSlugs.join(''));
assertNotMissingOrEmpty('trustedPrLabel', trustedPrLabel);
}
/**
* Check whether a PR contains files that are significant to the build.
* @param pr The number of the PR to check
* @param significantFilePattern A regex that selects files that are significant.
*/
public async getSignificantFilesChanged(pr: number, significantFilePattern: RegExp): Promise<boolean> {
const files = await this.prs.fetchFiles(pr);
return files.some(file => significantFilePattern.test(file.filename));
}
/**
* Check whether a PR is trusted.
* @param pr The number of the PR to check.
* @returns true if the PR is trusted.
*/
public async getPrIsTrusted(pr: number): Promise<boolean> {
const prInfo = await this.prs.fetch(pr);
return this.hasLabel(prInfo, this.trustedPrLabel) ||
(await this.teams.isMemberBySlug(prInfo.user.login, this.allowedTeamSlugs));
}
protected hasLabel(prInfo: PullRequest, label: string): boolean {
return prInfo.labels.some(labelObj => labelObj.name === label);
}
}

View File

@ -0,0 +1,41 @@
// Imports
import {AIO_DOWNLOADS_DIR} from '../common/constants';
import {
AIO_ARTIFACT_MAX_SIZE,
AIO_ARTIFACT_PATH,
AIO_BUILDS_DIR,
AIO_CIRCLE_CI_TOKEN,
AIO_DOMAIN_NAME,
AIO_GITHUB_ORGANIZATION,
AIO_GITHUB_REPO,
AIO_GITHUB_TEAM_SLUGS,
AIO_GITHUB_TOKEN,
AIO_PREVIEW_SERVER_HOSTNAME,
AIO_PREVIEW_SERVER_PORT,
AIO_SIGNIFICANT_FILES_PATTERN,
AIO_TRUSTED_PR_LABEL,
} from '../common/env-variables';
import {PreviewServerFactory} from './preview-server-factory';
// Run
_main();
// Functions
function _main(): void {
PreviewServerFactory
.create({
buildArtifactPath: AIO_ARTIFACT_PATH,
buildsDir: AIO_BUILDS_DIR,
circleCiToken: AIO_CIRCLE_CI_TOKEN,
domainName: AIO_DOMAIN_NAME,
downloadSizeLimit: AIO_ARTIFACT_MAX_SIZE,
downloadsDir: AIO_DOWNLOADS_DIR,
githubOrg: AIO_GITHUB_ORGANIZATION,
githubRepo: AIO_GITHUB_REPO,
githubTeamSlugs: AIO_GITHUB_TEAM_SLUGS.split(','),
githubToken: AIO_GITHUB_TOKEN,
significantFilesPattern: AIO_SIGNIFICANT_FILES_PATTERN,
trustedPrLabel: AIO_TRUSTED_PR_LABEL,
})
.listen(AIO_PREVIEW_SERVER_PORT, AIO_PREVIEW_SERVER_HOSTNAME);
}

View File

@ -1,8 +1,8 @@
// Classes // Classes
export class UploadError extends Error { export class PreviewServerError extends Error {
// Constructor // Constructor
constructor(public status: number = 500, message?: string) { constructor(public status: number = 500, message?: string) {
super(message); super(message);
Object.setPrototypeOf(this, UploadError.prototype); Object.setPrototypeOf(this, PreviewServerError.prototype);
} }
} }

View File

@ -0,0 +1,183 @@
// Imports
import * as bodyParser from 'body-parser';
import * as express from 'express';
import * as http from 'http';
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, createLogger} 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 = createLogger('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();
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();
// RESPOND TO IS-ALIVE PING
middleware.get(/^\/health-check\/?$/, (_req, res) => res.sendStatus(200));
// 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, success } = await buildRetriever.getGithubInfo(buildNum);
if (!success) {
res.sendStatus(204);
logger.log(`PR:${pr}, Build:${buildNum} - Skipping preview processing because this build did not succeed.`);
return;
}
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, new RegExp(cfg.significantFilesPattern))) {
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);
} 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;
}
}

View File

@ -0,0 +1,29 @@
import * as express from 'express';
import {promisify} from 'util';
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);
await promisify(res.end.bind(res))(err.message);
}
/**
* 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);
}

View File

@ -1,87 +0,0 @@
// Imports
import * as jwt from 'jsonwebtoken';
import {GithubPullRequests, PullRequest} from '../common/github-pull-requests';
import {GithubTeams} from '../common/github-teams';
import {assertNotMissingOrEmpty} from '../common/utils';
import {UploadError} from './upload-error';
// Interfaces - Types
interface JwtPayload {
slug: string;
'pull-request': number;
}
// Enums
export enum BUILD_VERIFICATION_STATUS {
verifiedAndTrusted,
verifiedNotTrusted,
}
// Classes
export class BuildVerifier {
// Properties - Protected
protected githubPullRequests: GithubPullRequests;
protected githubTeams: GithubTeams;
// Constructor
constructor(protected secret: string, githubToken: string, protected repoSlug: string, organization: string,
protected allowedTeamSlugs: string[], protected trustedPrLabel: string) {
assertNotMissingOrEmpty('secret', secret);
assertNotMissingOrEmpty('githubToken', githubToken);
assertNotMissingOrEmpty('repoSlug', repoSlug);
assertNotMissingOrEmpty('organization', organization);
assertNotMissingOrEmpty('allowedTeamSlugs', allowedTeamSlugs && allowedTeamSlugs.join(''));
assertNotMissingOrEmpty('trustedPrLabel', trustedPrLabel);
this.githubPullRequests = new GithubPullRequests(githubToken, repoSlug);
this.githubTeams = new GithubTeams(githubToken, organization);
}
// Methods - Public
public getPrIsTrusted(pr: number): Promise<boolean> {
return Promise.resolve().
then(() => this.githubPullRequests.fetch(pr)).
then(prInfo => this.hasLabel(prInfo, this.trustedPrLabel) ||
this.githubTeams.isMemberBySlug(prInfo.user.login, this.allowedTeamSlugs));
}
public verify(expectedPr: number, authHeader: string): Promise<BUILD_VERIFICATION_STATUS> {
return Promise.resolve().
then(() => this.extractJwtString(authHeader)).
then(jwtString => this.verifyJwt(expectedPr, jwtString)).
then(jwtPayload => this.verifyPr(jwtPayload['pull-request'])).
catch(err => { throw new UploadError(403, `Error while verifying upload for PR ${expectedPr}: ${err}`); });
}
// Methods - Protected
protected extractJwtString(input: string): string {
return input.replace(/^token +/i, '');
}
protected hasLabel(prInfo: PullRequest, label: string) {
return prInfo.labels.some(labelObj => labelObj.name === label);
}
protected verifyJwt(expectedPr: number, token: string): Promise<JwtPayload> {
return new Promise((resolve, reject) => {
jwt.verify(token, this.secret, {issuer: 'Travis CI, GmbH'}, (err, payload: JwtPayload) => {
if (err) {
reject(err.message || err);
} else if (payload.slug !== this.repoSlug) {
reject(`jwt slug invalid. expected: ${this.repoSlug}`);
} else if (payload['pull-request'] !== expectedPr) {
reject(`jwt pull-request invalid. expected: ${expectedPr}`);
} else {
resolve(payload);
}
});
});
}
protected verifyPr(pr: number): Promise<BUILD_VERIFICATION_STATUS> {
return this.getPrIsTrusted(pr).
then(isTrusted => Promise.resolve(isTrusted ?
BUILD_VERIFICATION_STATUS.verifiedAndTrusted :
BUILD_VERIFICATION_STATUS.verifiedNotTrusted));
}
}

View File

@ -1,39 +0,0 @@
// Imports
import {getEnvVar} from '../common/utils';
import {BuildVerifier} from './build-verifier';
// Run
_main();
// Functions
function _main() {
const secret = 'unused';
const githubToken = getEnvVar('AIO_GITHUB_TOKEN');
const repoSlug = getEnvVar('AIO_REPO_SLUG');
const organization = getEnvVar('AIO_GITHUB_ORGANIZATION');
const allowedTeamSlugs = getEnvVar('AIO_GITHUB_TEAM_SLUGS').split(',');
const trustedPrLabel = getEnvVar('AIO_TRUSTED_PR_LABEL');
const pr = +getEnvVar('AIO_PREVERIFY_PR');
const buildVerifier = new BuildVerifier(secret, githubToken, repoSlug, organization, allowedTeamSlugs,
trustedPrLabel);
// Exit codes:
// - 0: The PR can be automatically trusted (i.e. author belongs to trusted team or PR has the "trusted PR" label).
// - 1: An error occurred.
// - 2: The PR cannot be automatically trusted.
buildVerifier.getPrIsTrusted(pr).
then(isTrusted => {
if (!isTrusted) {
console.warn(
`The PR cannot be automatically verified, because it doesn't have the "${trustedPrLabel}" label and the ` +
`the author is not an active member of any of the following teams: ${allowedTeamSlugs.join(', ')}`);
}
process.exit(isTrusted ? 0 : 2);
}).
catch(err => {
console.error(err);
process.exit(1);
});
}

View File

@ -1,34 +0,0 @@
// Imports
import {getEnvVar} from '../common/utils';
import {uploadServerFactory} from './upload-server-factory';
// Constants
const AIO_BUILDS_DIR = getEnvVar('AIO_BUILDS_DIR');
const AIO_DOMAIN_NAME = getEnvVar('AIO_DOMAIN_NAME');
const AIO_GITHUB_ORGANIZATION = getEnvVar('AIO_GITHUB_ORGANIZATION');
const AIO_GITHUB_TEAM_SLUGS = getEnvVar('AIO_GITHUB_TEAM_SLUGS');
const AIO_GITHUB_TOKEN = getEnvVar('AIO_GITHUB_TOKEN');
const AIO_PREVIEW_DEPLOYMENT_TOKEN = getEnvVar('AIO_PREVIEW_DEPLOYMENT_TOKEN');
const AIO_REPO_SLUG = getEnvVar('AIO_REPO_SLUG');
const AIO_TRUSTED_PR_LABEL = getEnvVar('AIO_TRUSTED_PR_LABEL');
const AIO_UPLOAD_HOSTNAME = getEnvVar('AIO_UPLOAD_HOSTNAME');
const AIO_UPLOAD_PORT = +getEnvVar('AIO_UPLOAD_PORT');
// Run
_main();
// Functions
function _main() {
uploadServerFactory.
create({
buildsDir: AIO_BUILDS_DIR,
domainName: AIO_DOMAIN_NAME,
githubOrganization: AIO_GITHUB_ORGANIZATION,
githubTeamSlugs: AIO_GITHUB_TEAM_SLUGS.split(','),
githubToken: AIO_GITHUB_TOKEN,
repoSlug: AIO_REPO_SLUG,
secret: AIO_PREVIEW_DEPLOYMENT_TOKEN,
trustedPrLabel: AIO_TRUSTED_PR_LABEL,
}).
listen(AIO_UPLOAD_PORT, AIO_UPLOAD_HOSTNAME);
}

View File

@ -1,153 +0,0 @@
// Imports
import * as bodyParser from 'body-parser';
import * as express from 'express';
import * as http from 'http';
import {GithubPullRequests} from '../common/github-pull-requests';
import {assertNotMissingOrEmpty} from '../common/utils';
import {BuildCreator} from './build-creator';
import {ChangedPrVisibilityEvent, CreatedBuildEvent} from './build-events';
import {BUILD_VERIFICATION_STATUS, BuildVerifier} from './build-verifier';
import {UploadError} from './upload-error';
// Constants
const AUTHORIZATION_HEADER = 'AUTHORIZATION';
const X_FILE_HEADER = 'X-FILE';
// Interfaces - Types
interface UploadServerConfig {
buildsDir: string;
domainName: string;
githubOrganization: string;
githubTeamSlugs: string[];
githubToken: string;
repoSlug: string;
secret: string;
trustedPrLabel: string;
}
// Classes
class UploadServerFactory {
// Methods - Public
public create({
buildsDir,
domainName,
githubOrganization,
githubTeamSlugs,
githubToken,
repoSlug,
secret,
trustedPrLabel,
}: UploadServerConfig): http.Server {
assertNotMissingOrEmpty('domainName', domainName);
const buildVerifier = new BuildVerifier(secret, githubToken, repoSlug, githubOrganization, githubTeamSlugs,
trustedPrLabel);
const buildCreator = this.createBuildCreator(buildsDir, githubToken, repoSlug, domainName);
const middleware = this.createMiddleware(buildVerifier, buildCreator);
const httpServer = http.createServer(middleware as any);
httpServer.on('listening', () => {
const info = httpServer.address();
console.info(`Up and running (and listening on ${info.address}:${info.port})...`);
});
return httpServer;
}
// Methods - Protected
protected createBuildCreator(buildsDir: string, githubToken: string, repoSlug: string,
domainName: string): BuildCreator {
const buildCreator = new BuildCreator(buildsDir);
const githubPullRequests = new GithubPullRequests(githubToken, repoSlug);
const postPreviewsComment = (pr: number, shas: string[]) => {
const body = shas.
map(sha => `You can preview ${sha} at https://pr${pr}-${sha}.${domainName}/.`).
join('\n');
return githubPullRequests.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;
}
protected createMiddleware(buildVerifier: BuildVerifier, buildCreator: BuildCreator): express.Express {
const middleware = express();
const jsonParser = bodyParser.json();
middleware.get(/^\/create-build\/([1-9][0-9]*)\/([0-9a-f]{40})\/?$/, (req, res) => {
const pr = req.params[0];
const sha = req.params[1];
const archive = req.header(X_FILE_HEADER);
const authHeader = req.header(AUTHORIZATION_HEADER);
if (!authHeader) {
this.throwRequestError(401, `Missing or empty '${AUTHORIZATION_HEADER}' header`, req);
} else if (!archive) {
this.throwRequestError(400, `Missing or empty '${X_FILE_HEADER}' header`, req);
} else {
Promise.resolve().
then(() => buildVerifier.verify(+pr, authHeader)).
then(verStatus => verStatus === BUILD_VERIFICATION_STATUS.verifiedAndTrusted).
then(isPublic => buildCreator.create(pr, sha, archive, isPublic).
then(() => res.sendStatus(isPublic ? 201 : 202))).
catch(err => this.respondWithError(res, err));
}
});
middleware.get(/^\/health-check\/?$/, (_req, res) => res.sendStatus(200));
middleware.post(/^\/pr-updated\/?$/, jsonParser, (req, res) => {
const {action, number: prNo}: {action?: string, number?: number} = req.body;
const visMayHaveChanged = !action || (action === 'labeled') || (action === 'unlabeled');
if (!visMayHaveChanged) {
res.sendStatus(200);
} else if (!prNo) {
this.throwRequestError(400, `Missing or empty 'number' field`, req);
} else {
Promise.resolve().
then(() => buildVerifier.getPrIsTrusted(prNo)).
then(isPublic => buildCreator.updatePrVisibility(String(prNo), isPublic)).
then(() => res.sendStatus(200)).
catch(err => this.respondWithError(res, err));
}
});
middleware.all('*', req => this.throwRequestError(404, 'Unknown resource', req));
middleware.use((err: any, _req: any, res: express.Response, _next: any) => this.respondWithError(res, err));
return middleware;
}
protected respondWithError(res: express.Response, err: any) {
if (!(err instanceof UploadError)) {
err = new UploadError(500, String((err && err.message) || err));
}
const statusText = http.STATUS_CODES[err.status] || '???';
console.error(`Upload error: ${err.status} - ${statusText}`);
console.error(err.message);
res.status(err.status).end(err.message);
}
protected throwRequestError(status: number, error: string, req: express.Request) {
const message = `${error} in request: ${req.method} ${req.originalUrl}` +
(!req.body ? '' : ` ${JSON.stringify(req.body)}`);
throw new UploadError(status, message);
}
}
// Exports
export const uploadServerFactory = new UploadServerFactory();

View File

@ -1,16 +1,37 @@
// Using the values below, we can fake the response of the corresponding methods in tests. This is export const enum BuildNums {
// necessary, because the test upload-server will be running as a separate node process, so we will BUILD_INFO_ERROR = 1,
// not have direct access to the code (e.g. for mocking). BUILD_INFO_404,
// (See also 'lib/verify-setup/start-test-upload-server.ts'.) BUILD_INFO_BUILD_FAILED,
BUILD_INFO_INVALID_GH_ORG,
BUILD_INFO_INVALID_GH_REPO,
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,
}
/* tslint:disable: variable-name */ 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,
}
// Special values to be used as `authHeader` in `BuildVerifier#verify()`. export const SHA = '1234567890'.repeat(4);
export const BV_verify_error = 'FAKE_VERIFICATION_ERROR'; export const ALT_SHA = 'abcde'.repeat(8);
export const BV_verify_verifiedNotTrusted = 'FAKE_VERIFIED_NOT_TRUSTED'; export const SIMILAR_SHA = SHA.slice(0, -1) + 'A';
// Special values to be used as `pr` in `BuildVerifier#getPrIsTrusted()`.
export const BV_getPrIsTrusted_error = 32203;
export const BV_getPrIsTrusted_notTrusted = 72457;
/* tslint:enable: variable-name */

View File

@ -0,0 +1,10 @@
declare module 'delete-empty' {
interface Options {
dryRun: boolean;
verbose: boolean;
filter: (filePath: string) => boolean;
}
export default function deleteEmpty(cwd: string, options?: Options): Promise<string[]>;
export default function deleteEmpty(cwd: string, options?: Options, callback?: (err: any, deleted: string[]) => void): void;
export function sync(cwd: string, options?: Options): string[];
}

View File

@ -4,18 +4,14 @@ import * as fs from 'fs';
import * as http from 'http'; import * as http from 'http';
import * as path from 'path'; import * as path from 'path';
import * as shell from 'shelljs'; import * as shell from 'shelljs';
import {HIDDEN_DIR_PREFIX, SHORT_SHA_LEN} from '../common/constants'; import {AIO_DOWNLOADS_DIR, HIDDEN_DIR_PREFIX} from '../common/constants';
import {getEnvVar} from '../common/utils'; import {
AIO_BUILDS_DIR,
// Constans AIO_NGINX_PORT_HTTP,
const TEST_AIO_BUILDS_DIR = getEnvVar('TEST_AIO_BUILDS_DIR'); AIO_NGINX_PORT_HTTPS,
const TEST_AIO_NGINX_HOSTNAME = getEnvVar('TEST_AIO_NGINX_HOSTNAME'); AIO_WWW_USER,
const TEST_AIO_NGINX_PORT_HTTP = +getEnvVar('TEST_AIO_NGINX_PORT_HTTP'); } from '../common/env-variables';
const TEST_AIO_NGINX_PORT_HTTPS = +getEnvVar('TEST_AIO_NGINX_PORT_HTTPS'); import {computeShortSha, createLogger} from '../common/utils';
const TEST_AIO_UPLOAD_HOSTNAME = getEnvVar('TEST_AIO_UPLOAD_HOSTNAME');
const TEST_AIO_UPLOAD_MAX_SIZE = +getEnvVar('TEST_AIO_UPLOAD_MAX_SIZE');
const TEST_AIO_UPLOAD_PORT = +getEnvVar('TEST_AIO_UPLOAD_PORT');
const WWW_USER = getEnvVar('AIO_WWW_USER');
// Interfaces - Types // Interfaces - Types
export interface CmdResult { success: boolean; err: Error | null; stdout: string; stderr: string; } export interface CmdResult { success: boolean; err: Error | null; stdout: string; stderr: string; }
@ -27,61 +23,50 @@ export type VerifyCmdResultFn = (result: CmdResult) => void;
// Classes // Classes
class Helper { class Helper {
// Properties - Public
public get buildsDir() { return TEST_AIO_BUILDS_DIR; }
public get nginxHostname() { return TEST_AIO_NGINX_HOSTNAME; }
public get nginxPortHttp() { return TEST_AIO_NGINX_PORT_HTTP; }
public get nginxPortHttps() { return TEST_AIO_NGINX_PORT_HTTPS; }
public get uploadHostname() { return TEST_AIO_UPLOAD_HOSTNAME; }
public get uploadPort() { return TEST_AIO_UPLOAD_PORT; }
public get uploadMaxSize() { return TEST_AIO_UPLOAD_MAX_SIZE; }
public get wwwUser() { return WWW_USER; }
// Properties - Protected // Properties - Protected
protected cleanUpFns: CleanUpFn[] = []; protected cleanUpFns: CleanUpFn[] = [];
protected portPerScheme: {[scheme: string]: number} = { protected portPerScheme: {[scheme: string]: number} = {
http: this.nginxPortHttp, http: AIO_NGINX_PORT_HTTP,
https: this.nginxPortHttps, https: AIO_NGINX_PORT_HTTPS,
}; };
private logger = createLogger('TestHelper');
// Constructor // Constructor
constructor() { constructor() {
shell.mkdir('-p', this.buildsDir); shell.mkdir('-p', AIO_BUILDS_DIR);
shell.exec(`chown -R ${this.wwwUser} ${this.buildsDir}`); 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 // Methods - Public
public buildExists(pr: string, sha = '', isPublic = true, legacy = false): boolean { public cleanUp(): void {
const prDir = this.getPrDir(pr, isPublic);
const dir = !sha ? prDir : this.getShaDir(prDir, sha, legacy);
return fs.existsSync(dir);
}
public cleanUp() {
while (this.cleanUpFns.length) { while (this.cleanUpFns.length) {
// Clean-up fns remove themselves from the list. // Clean-up fns remove themselves from the list.
this.cleanUpFns[0](); this.cleanUpFns[0]();
} }
if (fs.readdirSync(this.buildsDir).length) { const leftoverDownloads = fs.readdirSync(AIO_DOWNLOADS_DIR);
throw new Error(`Directory '${this.buildsDir}' is not empty after clean-up.`); 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 createDummyArchive(pr: string, sha: string, archivePath: string): CleanUpFn { public createDummyBuild(pr: number, sha: string, isPublic = true, force = false, legacy = false): CleanUpFn {
const inputDir = this.getShaDir(this.getPrDir(`uploaded/${pr}`, true), sha);
const cmd1 = `tar --create --gzip --directory "${inputDir}" --file "${archivePath}" .`;
const cmd2 = `chown ${this.wwwUser} ${archivePath}`;
const cleanUpTemp = this.createDummyBuild(`uploaded/${pr}`, sha, true, true);
shell.exec(cmd1);
shell.exec(cmd2);
cleanUpTemp();
return this.createCleanUpFn(() => shell.rm('-rf', archivePath));
}
public createDummyBuild(pr: string, sha: string, isPublic = true, force = false, legacy = false): CleanUpFn {
const prDir = this.getPrDir(pr, isPublic); const prDir = this.getPrDir(pr, isPublic);
const shaDir = this.getShaDir(prDir, sha, legacy); const shaDir = this.getShaDir(prDir, sha, legacy);
const idxPath = path.join(shaDir, 'index.html'); const idxPath = path.join(shaDir, 'index.html');
@ -89,35 +74,21 @@ class Helper {
this.writeFile(idxPath, {content: `PR: ${pr} | SHA: ${sha} | File: /index.html`}, force); 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); this.writeFile(barPath, {content: `PR: ${pr} | SHA: ${sha} | File: /foo/bar.js`}, force);
shell.exec(`chown -R ${this.wwwUser} ${prDir}`); shell.exec(`chown -R ${AIO_WWW_USER} ${prDir}`);
return this.createCleanUpFn(() => shell.rm('-rf', prDir)); return this.createCleanUpFn(() => shell.rm('-rf', prDir));
} }
public deletePrDir(pr: string, isPublic = true) { public getPrDir(pr: number, isPublic: boolean): string {
const prDir = this.getPrDir(pr, isPublic); const prDirName = isPublic ? '' + pr : HIDDEN_DIR_PREFIX + pr;
return path.join(AIO_BUILDS_DIR, prDirName);
if (fs.existsSync(prDir)) {
// Undocumented signature (see https://github.com/shelljs/shelljs/pull/663).
(shell as any).chmod('-R', 'a+w', prDir);
shell.rm('-rf', prDir);
}
}
public getPrDir(pr: string, isPublic: boolean): string {
const prDirName = isPublic ? pr : HIDDEN_DIR_PREFIX + pr;
return path.join(this.buildsDir, prDirName);
} }
public getShaDir(prDir: string, sha: string, legacy = false): string { public getShaDir(prDir: string, sha: string, legacy = false): string {
return path.join(prDir, legacy ? sha : this.getShordSha(sha)); return path.join(prDir, legacy ? sha : computeShortSha(sha));
} }
public getShordSha(sha: string): string { public readBuildFile(pr: number, sha: string, relFilePath: string, isPublic = true, legacy = false): string {
return sha.substr(0, SHORT_SHA_LEN);
}
public readBuildFile(pr: string, sha: string, relFilePath: string, isPublic = true, legacy = false): string {
const shaDir = this.getShaDir(this.getPrDir(pr, isPublic), sha, legacy); const shaDir = this.getShaDir(this.getPrDir(pr, isPublic), sha, legacy);
const absFilePath = path.join(shaDir, relFilePath); const absFilePath = path.join(shaDir, relFilePath);
return fs.readFileSync(absFilePath, 'utf8'); return fs.readFileSync(absFilePath, 'utf8');
@ -130,7 +101,7 @@ class Helper {
}); });
} }
public runForAllSupportedSchemes(suiteFactory: TestSuiteFactory) { public runForAllSupportedSchemes(suiteFactory: TestSuiteFactory): void {
Object.keys(this.portPerScheme).forEach(scheme => suiteFactory(scheme, this.portPerScheme[scheme])); Object.keys(this.portPerScheme).forEach(scheme => suiteFactory(scheme, this.portPerScheme[scheme]));
} }
@ -154,9 +125,9 @@ class Helper {
// Only keep the last to sections (final headers and body). // Only keep the last to sections (final headers and body).
if (!result.success) { if (!result.success) {
console.log('Stdout:', result.stdout); this.logger.log('Stdout:', result.stdout);
console.log('Stderr:', result.stderr); this.logger.error('Stderr:', result.stderr);
console.log('Error:', result.err); this.logger.error('Error:', result.err);
} }
expect(result.success).toBe(true); expect(result.success).toBe(true);
@ -165,14 +136,14 @@ class Helper {
}; };
} }
public writeBuildFile(pr: string, sha: string, relFilePath: string, content: string, isPublic = true, public writeBuildFile(pr: number, sha: string, relFilePath: string, content: string, isPublic = true,
legacy = false): CleanUpFn { legacy = false): void {
const shaDir = this.getShaDir(this.getPrDir(pr, isPublic), sha, legacy); const shaDir = this.getShaDir(this.getPrDir(pr, isPublic), sha, legacy);
const absFilePath = path.join(shaDir, relFilePath); const absFilePath = path.join(shaDir, relFilePath);
return this.writeFile(absFilePath, {content}, true); this.writeFile(absFilePath, {content}, true);
} }
public writeFile(filePath: string, {content, size}: FileSpecs, force = false): CleanUpFn { public writeFile(filePath: string, {content, size}: FileSpecs, force = false): void {
if (!force && fs.existsSync(filePath)) { if (!force && fs.existsSync(filePath)) {
throw new Error(`Refusing to overwrite existing file '${filePath}'.`); throw new Error(`Refusing to overwrite existing file '${filePath}'.`);
} }
@ -190,9 +161,7 @@ class Helper {
// Create a file with the specified content. // Create a file with the specified content.
fs.writeFileSync(filePath, content || ''); fs.writeFileSync(filePath, content || '');
} }
shell.exec(`chown ${this.wwwUser} ${filePath}`); shell.exec(`chown ${AIO_WWW_USER} ${filePath}`);
return this.createCleanUpFn(() => shell.rm('-rf', cleanUpTarget));
} }
// Methods - Protected // Methods - Protected
@ -211,5 +180,54 @@ class Helper {
} }
} }
interface CurlOptions {
method?: string;
options?: string;
data?: any;
url?: string;
extraPath?: string;
}
export function makeCurl(baseUrl: string) {
return function curl({
method = 'POST',
options = '',
data = {},
url = baseUrl,
extraPath = '',
}: CurlOptions) {
const dataString = data ? JSON.stringify(data) : '';
const cmd = `curl -iLX ${method} ` +
`${options} ` +
`--header "Content-Type: application/json" ` +
`--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 // Exports
export const helper = new Helper(); export const helper = new Helper();

View File

@ -0,0 +1,7 @@
declare module jasmine {
interface Matchers {
toExistAsAFile(remove = true): boolean;
toExistAsABuild(remove = true): boolean;
toExistAsAnArtifact(remove = true): boolean;
}
}

View File

@ -0,0 +1,88 @@
import {sync as deleteEmpty} from 'delete-empty';
import {existsSync, unlinkSync} from 'fs';
import {join} from 'path';
import {AIO_DOWNLOADS_DIR} from '../common/constants';
import {computeShortSha} from '../common/utils';
import {SHA} from './constants';
import {helper} from './helper';
function checkFile(filePath: string, remove: boolean): boolean {
const exists = existsSync(filePath);
if (exists && remove) {
// if we expected the file to exist then we remove it to prevent leftover file errors
unlinkSync(filePath);
}
return exists;
}
function getArtifactPath(prNum: number, sha: string = SHA): string {
return `${AIO_DOWNLOADS_DIR}/${prNum}-${computeShortSha(sha)}-aio-snapshot.tgz`;
}
function checkFiles(prNum: number, isPublic: boolean, sha: string, isLegacy: boolean, remove: boolean) {
const files = ['/index.html', '/foo/bar.js'];
const prPath = helper.getPrDir(prNum, isPublic);
const shaPath = helper.getShaDir(prPath, sha, isLegacy);
const existingFiles: string[] = [];
const missingFiles: string[] = [];
files
.map(file => join(shaPath, file))
.forEach(file => (checkFile(file, remove) ? existingFiles : missingFiles).push(file));
deleteEmpty(prPath);
return { existingFiles, missingFiles };
}
class ToExistAsAFile implements jasmine.CustomMatcher {
public compare(actual: string, remove = true): jasmine.CustomMatcherResult {
const pass = checkFile(actual, remove);
return {
message: `Expected file at "${actual}" ${pass ? 'not' : ''} to exist`,
pass,
};
}
}
class ToExistAsAnArtifact implements jasmine.CustomMatcher {
public compare(actual: {prNum: number, sha?: string}, remove = true): jasmine.CustomMatcherResult {
const { prNum, sha = SHA } = actual;
const filePath = getArtifactPath(prNum, sha);
const pass = checkFile(filePath, remove);
return {
message: `Expected artifact "PR:${prNum}, SHA:${sha}, FILE:${filePath}" ${pass ? 'not' : '\b'} to exist`,
pass,
};
}
}
class ToExistAsABuild implements jasmine.CustomMatcher {
public compare(actual: {prNum: number, isPublic?: boolean, sha?: string, isLegacy?: boolean}, remove = true):
jasmine.CustomMatcherResult {
const {prNum, isPublic = true, sha = SHA, isLegacy = false} = actual;
const {missingFiles} = checkFiles(prNum, isPublic, sha, isLegacy, remove);
return {
message: `Expected files for build "PR:${prNum}, SHA:${sha}" to exist:\n` +
missingFiles.map(file => ` - ${file}`).join('\n'),
pass: missingFiles.length === 0,
};
}
public negativeCompare(actual: {prNum: number, isPublic?: boolean, sha?: string, isLegacy?: boolean}):
jasmine.CustomMatcherResult {
const {prNum, isPublic = true, sha = SHA, isLegacy = false} = actual;
const { existingFiles } = checkFiles(prNum, isPublic, sha, isLegacy, false);
return {
message: `Expected files for build "PR:${prNum}, SHA:${sha}" not to exist:\n` +
existingFiles.map(file => ` - ${file}`).join('\n'),
pass: existingFiles.length === 0,
};
}
}
export const customMatchers = {
toExistAsABuild: () => new ToExistAsABuild(),
toExistAsAFile: () => new ToExistAsAFile(),
toExistAsAnArtifact: () => new ToExistAsAnArtifact(),
};

View File

@ -0,0 +1,171 @@
/* tslint:disable:max-line-length */
import * as nock from 'nock';
import * as tar from 'tar-stream';
import {gzipSync} from 'zlib';
import {createLogger, getEnvVar} from '../common/utils';
import {BuildNums, 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 = createLogger('NOCK');
const log = (...args: any[]) => {
// Filter out non-matching URL checks
if (!/^matching.+: false$/.test(args[0])) {
logger.log(...args);
}
};
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 = {
branch: `pull/${PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER}`,
failed: false,
reponame: AIO_GITHUB_REPO,
username: AIO_GITHUB_ORGANIZATION,
vcs_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 CIRCLE_CI_TOKEN_PARAM = `circle-token=${AIO_CIRCLE_CI_TOKEN}`;
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/v1.1/project/github/${AIO_GITHUB_ORGANIZATION}/${AIO_GITHUB_REPO}`;
const buildInfoUrl = (buildNum: number) => `${CIRCLE_CI_BUILD_INFO_URL}/${buildNum}?${CIRCLE_CI_TOKEN_PARAM}`;
const buildArtifactsUrl = (buildNum: number) => `${CIRCLE_CI_BUILD_INFO_URL}/${buildNum}/artifacts?${CIRCLE_CI_TOKEN_PARAM}`;
const buildInfo = (prNum: number) => ({ ...BASIC_BUILD_INFO, branch: `pull/${prNum}` });
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) => `${GITHUB_PULLS_URL}/${prNum}/files`;
const getCommentUrl = (prNum: number) => `${getIssueUrl(prNum)}/comments`;
const getTeamMembershipUrl = (teamId: number, username: string) => `/teams/${teamId}/memberships/${username}`;
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).log(log).persist();
const githubApi = nock(GITHUB_API_HOST).log(log).persist().matchHeader('Authorization', `token ${AIO_GITHUB_TOKEN}`);
//////////////////////////////
// GENERAL responses
githubApi.get(GITHUB_TEAMS_URL + '?page=0&per_page=100').reply(200, TEST_TEAM_INFO);
githubApi.post(getCommentUrl(PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER)).reply(200);
// 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_BUILD_FAILED)).reply(200, { ...BASIC_BUILD_INFO, failed: true });
circleCiApi.get(buildInfoUrl(BuildNums.BUILD_INFO_INVALID_GH_ORG)).reply(200, { ...BASIC_BUILD_INFO, username: 'bad' });
circleCiApi.get(buildInfoUrl(BuildNums.BUILD_INFO_INVALID_GH_REPO)).reply(200, { ...BASIC_BUILD_INFO, reponame: 'bad' });
// CHANGED FILE errors
circleCiApi.get(buildInfoUrl(BuildNums.CHANGED_FILES_ERROR)).reply(200, buildInfo(PrNums.CHANGED_FILES_ERROR));
githubApi.get(getFilesUrl(PrNums.CHANGED_FILES_ERROR)).replyWithError('CHANGED_FILES_ERROR');
circleCiApi.get(buildInfoUrl(BuildNums.CHANGED_FILES_404)).reply(200, buildInfo(PrNums.CHANGED_FILES_404));
githubApi.get(getFilesUrl(PrNums.CHANGED_FILES_404)).reply(404, 'CHANGED_FILES_404');
circleCiApi.get(buildInfoUrl(BuildNums.CHANGED_FILES_NONE)).reply(200, buildInfo(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, buildInfo(PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER));
circleCiApi.get(buildArtifactsUrl(BuildNums.BUILD_ARTIFACTS_ERROR)).replyWithError('BUILD_ARTIFACTS_ERROR');
circleCiApi.get(buildInfoUrl(BuildNums.BUILD_ARTIFACTS_404)).reply(200, buildInfo(PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER));
circleCiApi.get(buildArtifactsUrl(BuildNums.BUILD_ARTIFACTS_404)).reply(404, 'BUILD_ARTIFACTS_ERROR');
circleCiApi.get(buildInfoUrl(BuildNums.BUILD_ARTIFACTS_EMPTY)).reply(200, buildInfo(PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER));
circleCiApi.get(buildArtifactsUrl(BuildNums.BUILD_ARTIFACTS_EMPTY)).reply(200, []);
circleCiApi.get(buildInfoUrl(BuildNums.BUILD_ARTIFACTS_MISSING)).reply(200, buildInfo(PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER));
circleCiApi.get(buildArtifactsUrl(BuildNums.BUILD_ARTIFACTS_MISSING)).reply(200, [ARTIFACT_1, ARTIFACT_2, ARTIFACT_3]);
// ARTIFACT DOWNLOAD errors
circleCiApi.get(buildInfoUrl(BuildNums.DOWNLOAD_ARTIFACT_ERROR)).reply(200, buildInfo(PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER));
circleCiApi.get(buildArtifactsUrl(BuildNums.DOWNLOAD_ARTIFACT_ERROR)).reply(200, [ARTIFACT_ERROR]);
circleCiApi.get(ARTIFACT_ERROR._urlPath).replyWithError(ARTIFACT_ERROR._urlPath);
circleCiApi.get(buildInfoUrl(BuildNums.DOWNLOAD_ARTIFACT_404)).reply(200, buildInfo(PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER));
circleCiApi.get(buildArtifactsUrl(BuildNums.DOWNLOAD_ARTIFACT_404)).reply(200, [ARTIFACT_404]);
circleCiApi.get(ARTIFACT_ERROR._urlPath).reply(404, ARTIFACT_ERROR._urlPath);
// TRUST CHECK errors
circleCiApi.get(buildInfoUrl(BuildNums.TRUST_CHECK_ERROR)).reply(200, buildInfo(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, [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, [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, [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, [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
circleCiApi.get(buildInfoUrl(BuildNums.TRUST_CHECK_UNTRUSTED)).reply(200, buildInfo(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, [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);

View File

@ -1,17 +1,22 @@
// Imports // Imports
import * as path from 'path'; 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 {helper as h} from './helper'; import {helper as h} from './helper';
import {customMatchers} from './jasmine-custom-matchers';
// Tests // Tests
describe(`nginx`, () => { describe(`nginx`, () => {
beforeEach(() => jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000); beforeEach(() => jasmine.DEFAULT_TIMEOUT_INTERVAL = 5000);
beforeEach(() => jasmine.addMatchers(customMatchers));
afterEach(() => h.cleanUp()); afterEach(() => h.cleanUp());
it('should redirect HTTP to HTTPS', done => { it('should redirect HTTP to HTTPS', done => {
const httpHost = `${h.nginxHostname}:${h.nginxPortHttp}`; const httpHost = `${AIO_NGINX_HOSTNAME}:${AIO_NGINX_PORT_HTTP}`;
const httpsHost = `${h.nginxHostname}:${h.nginxPortHttps}`; const httpsHost = `${AIO_NGINX_HOSTNAME}:${AIO_NGINX_PORT_HTTPS}`;
const urlMap = { const urlMap = {
[`http://${httpHost}/`]: `https://${httpsHost}/`, [`http://${httpHost}/`]: `https://${httpsHost}/`,
[`http://${httpHost}/foo`]: `https://${httpsHost}/foo`, [`http://${httpHost}/foo`]: `https://${httpsHost}/foo`,
@ -32,13 +37,13 @@ describe(`nginx`, () => {
h.runForAllSupportedSchemes((scheme, port) => describe(`(on ${scheme.toUpperCase()})`, () => { h.runForAllSupportedSchemes((scheme, port) => describe(`(on ${scheme.toUpperCase()})`, () => {
const hostname = h.nginxHostname; const hostname = AIO_NGINX_HOSTNAME;
const host = `${hostname}:${port}`; const host = `${hostname}:${port}`;
const pr = '9'; const pr = 9;
const sha9 = '9'.repeat(40); const sha9 = '9'.repeat(40);
const sha0 = '0'.repeat(40); const sha0 = '0'.repeat(40);
const shortSha9 = h.getShordSha(sha9); const shortSha9 = computeShortSha(sha9);
const shortSha0 = h.getShordSha(sha0); const shortSha0 = computeShortSha(sha0);
describe(`pr<pr>-<sha>.${host}/*`, () => { describe(`pr<pr>-<sha>.${host}/*`, () => {
@ -50,6 +55,11 @@ describe(`nginx`, () => {
h.createDummyBuild(pr, sha0); h.createDummyBuild(pr, sha0);
}); });
afterEach(() => {
expect({ prNum: pr, sha: sha9 }).toExistAsABuild();
expect({ prNum: pr, sha: sha0 }).toExistAsABuild();
});
it('should return /index.html', done => { it('should return /index.html', done => {
const origin = `${scheme}://pr${pr}-${shortSha9}.${host}`; const origin = `${scheme}://pr${pr}-${shortSha9}.${host}`;
@ -63,17 +73,19 @@ describe(`nginx`, () => {
}); });
it('should return /index.html (for legacy builds)', done => { it('should return /index.html (for legacy builds)', async () => {
const origin = `${scheme}://pr${pr}-${sha9}.${host}`; const origin = `${scheme}://pr${pr}-${sha9}.${host}`;
const bodyRegex = new RegExp(`^PR: ${pr} | SHA: ${sha9} | File: /index\\.html$`); const bodyRegex = new RegExp(`^PR: ${pr} | SHA: ${sha9} | File: /index\\.html$`);
h.createDummyBuild(pr, sha9, true, false, true); h.createDummyBuild(pr, sha9, true, false, true);
Promise.all([ await Promise.all([
h.runCmd(`curl -iL ${origin}/index.html`).then(h.verifyResponse(200, bodyRegex)), 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)),
h.runCmd(`curl -iL ${origin}`).then(h.verifyResponse(200, bodyRegex)), h.runCmd(`curl -iL ${origin}`).then(h.verifyResponse(200, bodyRegex)),
]).then(done); ]);
expect({ prNum: pr, sha: sha9, isLegacy: true }).toExistAsABuild();
}); });
@ -86,15 +98,15 @@ describe(`nginx`, () => {
}); });
it('should return /foo/bar.js (for legacy builds)', done => { it('should return /foo/bar.js (for legacy builds)', async () => {
const origin = `${scheme}://pr${pr}-${sha9}.${host}`; const origin = `${scheme}://pr${pr}-${sha9}.${host}`;
const bodyRegex = new RegExp(`^PR: ${pr} | SHA: ${sha9} | File: /foo/bar\\.js$`); const bodyRegex = new RegExp(`^PR: ${pr} | SHA: ${sha9} | File: /foo/bar\\.js$`);
h.createDummyBuild(pr, sha9, true, false, true); h.createDummyBuild(pr, sha9, true, false, true);
h.runCmd(`curl -iL ${origin}/foo/bar.js`). await h.runCmd(`curl -iL ${origin}/foo/bar.js`).then(h.verifyResponse(200, bodyRegex));
then(h.verifyResponse(200, bodyRegex)).
then(done); expect({ prNum: pr, sha: sha9, isLegacy: true }).toExistAsABuild();
}); });
@ -126,7 +138,7 @@ describe(`nginx`, () => {
it('should respond with 404 for unknown PRs/SHAs', done => { it('should respond with 404 for unknown PRs/SHAs', done => {
const otherPr = 54321; const otherPr = 54321;
const otherShortSha = h.getShordSha('8'.repeat(40)); const otherShortSha = computeShortSha('8'.repeat(40));
Promise.all([ Promise.all([
h.runCmd(`curl -iL ${scheme}://pr${pr}9-${shortSha9}.${host}`).then(h.verifyResponse(404)), h.runCmd(`curl -iL ${scheme}://pr${pr}9-${shortSha9}.${host}`).then(h.verifyResponse(404)),
@ -174,39 +186,41 @@ describe(`nginx`, () => {
describe('(for hidden builds)', () => { describe('(for hidden builds)', () => {
it('should respond with 404 for any file or directory', done => { it('should respond with 404 for any file or directory', async () => {
const origin = `${scheme}://pr${pr}-${shortSha9}.${host}`; const origin = `${scheme}://pr${pr}-${shortSha9}.${host}`;
const assert404 = h.verifyResponse(404); const assert404 = h.verifyResponse(404);
h.createDummyBuild(pr, sha9, false); h.createDummyBuild(pr, sha9, false);
expect(h.buildExists(pr, sha9, false)).toBe(true);
Promise.all([ await Promise.all([
h.runCmd(`curl -iL ${origin}/index.html`).then(assert404), 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}`).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/bar.js`).then(assert404),
h.runCmd(`curl -iL ${origin}/foo/`).then(assert404), h.runCmd(`curl -iL ${origin}/foo/`).then(assert404),
h.runCmd(`curl -iL ${origin}/foo`).then(assert404), h.runCmd(`curl -iL ${origin}/foo`).then(assert404),
]).then(done); ]);
expect({ prNum: pr, sha: sha9, isPublic: false }).toExistAsABuild();
}); });
it('should respond with 404 for any file or directory (for legacy builds)', done => { it('should respond with 404 for any file or directory (for legacy builds)', async () => {
const origin = `${scheme}://pr${pr}-${sha9}.${host}`; const origin = `${scheme}://pr${pr}-${sha9}.${host}`;
const assert404 = h.verifyResponse(404); const assert404 = h.verifyResponse(404);
h.createDummyBuild(pr, sha9, false, false, true); h.createDummyBuild(pr, sha9, false, false, true);
expect(h.buildExists(pr, sha9, false, true)).toBe(true);
Promise.all([ await Promise.all([
h.runCmd(`curl -iL ${origin}/index.html`).then(assert404), 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}`).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/bar.js`).then(assert404),
h.runCmd(`curl -iL ${origin}/foo/`).then(assert404), h.runCmd(`curl -iL ${origin}/foo/`).then(assert404),
h.runCmd(`curl -iL ${origin}/foo`).then(assert404), h.runCmd(`curl -iL ${origin}/foo`).then(assert404),
]).then(done); ]);
expect({ prNum: pr, sha: sha9, isPublic: false, isLegacy: true }).toExistAsABuild();
}); });
}); });
@ -238,10 +252,10 @@ describe(`nginx`, () => {
}); });
describe(`${host}/create-build/<pr>/<sha>`, () => { describe(`${host}/circle-build`, () => {
it('should disallow non-POST requests', done => { it('should disallow non-POST requests', done => {
const url = `${scheme}://${host}/create-build/${pr}/${sha9}`; const url = `${scheme}://${host}/circle-build`;
Promise.all([ Promise.all([
h.runCmd(`curl -iLX GET ${url}`).then(h.verifyResponse([405, 'Not Allowed'])), h.runCmd(`curl -iLX GET ${url}`).then(h.verifyResponse([405, 'Not Allowed'])),
@ -252,31 +266,9 @@ describe(`nginx`, () => {
}); });
it(`should reject files larger than ${h.uploadMaxSize}B (according to header)`, done => { it('should pass requests through to the preview server', done => {
const headers = `--header "Content-Length: ${1.5 * h.uploadMaxSize}"`; h.runCmd(`curl -iLX POST ${scheme}://${host}/circle-build`).
const url = `${scheme}://${host}/create-build/${pr}/${sha9}`; then(h.verifyResponse(400, /Incorrect body content. Expected JSON/)).
h.runCmd(`curl -iLX POST ${headers} ${url}`).
then(h.verifyResponse([413, 'Request Entity Too Large'])).
then(done);
});
it(`should reject files larger than ${h.uploadMaxSize}B (without header)`, done => {
const filePath = path.join(h.buildsDir, 'snapshot.tar.gz');
const url = `${scheme}://${host}/create-build/${pr}/${sha9}`;
h.writeFile(filePath, {size: 1.5 * h.uploadMaxSize});
h.runCmd(`curl -iLX POST --data-binary "@${filePath}" ${url}`).
then(h.verifyResponse([413, 'Request Entity Too Large'])).
then(done);
});
it('should pass requests through to the upload server', done => {
h.runCmd(`curl -iLX POST ${scheme}://${host}/create-build/${pr}/${sha9}`).
then(h.verifyResponse(401, /Missing or empty 'AUTHORIZATION' header/)).
then(done); then(done);
}); });
@ -285,35 +277,16 @@ describe(`nginx`, () => {
const cmdPrefix = `curl -iLX POST ${scheme}://${host}`; const cmdPrefix = `curl -iLX POST ${scheme}://${host}`;
Promise.all([ Promise.all([
h.runCmd(`${cmdPrefix}/foo/create-build/${pr}/${sha9}`).then(h.verifyResponse(404)), h.runCmd(`${cmdPrefix}/foo/circle-build/`).then(h.verifyResponse(404)),
h.runCmd(`${cmdPrefix}/foo-create-build/${pr}/${sha9}`).then(h.verifyResponse(404)), h.runCmd(`${cmdPrefix}/foo-circle-build/`).then(h.verifyResponse(404)),
h.runCmd(`${cmdPrefix}/fooncreate-build/${pr}/${sha9}`).then(h.verifyResponse(404)), h.runCmd(`${cmdPrefix}/fooncircle-build/`).then(h.verifyResponse(404)),
h.runCmd(`${cmdPrefix}/create-build/foo/${pr}/${sha9}`).then(h.verifyResponse(404)), h.runCmd(`${cmdPrefix}/circle-build/foo/`).then(h.verifyResponse(404)),
h.runCmd(`${cmdPrefix}/create-build-foo/${pr}/${sha9}`).then(h.verifyResponse(404)), h.runCmd(`${cmdPrefix}/circle-build-foo/`).then(h.verifyResponse(404)),
h.runCmd(`${cmdPrefix}/create-buildnfoo/${pr}/${sha9}`).then(h.verifyResponse(404)), h.runCmd(`${cmdPrefix}/circle-buildnfoo/`).then(h.verifyResponse(404)),
h.runCmd(`${cmdPrefix}/create-build/pr${pr}/${sha9}`).then(h.verifyResponse(404)), h.runCmd(`${cmdPrefix}/circle-build/pr`).then(h.verifyResponse(404)),
h.runCmd(`${cmdPrefix}/create-build/${pr}/${sha9}42`).then(h.verifyResponse(404)), h.runCmd(`${cmdPrefix}/circle-build/42`).then(h.verifyResponse(404)),
]).then(done); ]).then(done);
}); });
it('should reject PRs with leading zeros', done => {
h.runCmd(`curl -iLX POST ${scheme}://${host}/create-build/0${pr}/${sha9}`).
then(h.verifyResponse(404)).
then(done);
});
it('should accept SHAs with leading zeros (but not trim the zeros)', done => {
const cmdPrefix = `curl -iLX POST ${scheme}://${host}/create-build/${pr}`;
const bodyRegex = /Missing or empty 'AUTHORIZATION' header/;
Promise.all([
h.runCmd(`${cmdPrefix}/0${sha9}`).then(h.verifyResponse(404)),
h.runCmd(`${cmdPrefix}/${sha0}`).then(h.verifyResponse(401, bodyRegex)),
]).then(done);
});
}); });
@ -331,17 +304,13 @@ describe(`nginx`, () => {
}); });
it('should pass requests through to the upload server', done => { it('should pass requests through to the preview server', done => {
const cmdPrefix = `curl -iLX POST --header "Content-Type: application/json"`; const cmdPrefix = `curl -iLX POST --header "Content-Type: application/json"`;
const cmd1 = `${cmdPrefix} ${url}`; const cmd1 = `${cmdPrefix} ${url}`;
const cmd2 = `${cmdPrefix} --data '{"number":${pr}}' ${url}`;
const cmd3 = `${cmdPrefix} --data '{"number":${pr},"action":"foo"}' ${url}`;
Promise.all([ Promise.all([
h.runCmd(cmd1).then(h.verifyResponse(400, /Missing or empty 'number' field/)), h.runCmd(cmd1).then(h.verifyResponse(400, /Missing or empty 'number' field/)),
h.runCmd(cmd2).then(h.verifyResponse(200)),
h.runCmd(cmd3).then(h.verifyResponse(200)),
]).then(done); ]).then(done);
}); });
@ -364,13 +333,15 @@ describe(`nginx`, () => {
describe(`${host}/*`, () => { describe(`${host}/*`, () => {
it('should respond with 404 for unknown URLs (even if the resource exists)', done => { beforeEach(() => {
['index.html', 'foo.js', 'foo/index.html'].forEach(relFilePath => { ['index.html', 'foo.js', 'foo/index.html'].forEach(relFilePath => {
const absFilePath = path.join(h.buildsDir, relFilePath); const absFilePath = path.join(AIO_BUILDS_DIR, relFilePath);
h.writeFile(absFilePath, {content: `File: /${relFilePath}`}); return h.writeFile(absFilePath, {content: `File: /${relFilePath}`});
});
}); });
Promise.all([ 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}/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}://${host}`).then(h.verifyResponse(404)), h.runCmd(`curl -iL ${scheme}://${host}`).then(h.verifyResponse(404)),
@ -379,7 +350,14 @@ describe(`nginx`, () => {
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.js`).then(h.verifyResponse(404)),
h.runCmd(`curl -iL ${scheme}://${host}/foo/index.html`).then(h.verifyResponse(404)), h.runCmd(`curl -iL ${scheme}://${host}/foo/index.html`).then(h.verifyResponse(404)),
]).then(done); ]);
});
afterEach(() => {
['index.html', 'foo.js', 'foo/index.html', 'foo'].forEach(relFilePath => {
const absFilePath = path.join(AIO_BUILDS_DIR, relFilePath);
rm('-r', absFilePath);
});
}); });
}); });

View File

@ -0,0 +1,483 @@
// 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}/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 the CircleCI API request errors', async () => {
await curl(payload(BuildNums.BUILD_INFO_ERROR)).then(h.verifyResponse(500));
await curl(payload(BuildNums.BUILD_INFO_404)).then(h.verifyResponse(500));
});
it('should respond with 204 if the build on CircleCI failed', async () => {
await curl(payload(BuildNums.BUILD_INFO_BUILD_FAILED)).then(h.verifyResponse(204));
});
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].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', done => {
Promise.all([
h.runCmd(`curl -iL ${host}/health-check`).then(h.verifyResponse(200)),
h.runCmd(`curl -iL ${host}/health-check/`).then(h.verifyResponse(200)),
]).then(done);
});
it('should respond with 404 if the path does not match exactly', done => {
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)),
]).then(done);
});
});
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', done => {
const mockPayload = JSON.stringify({number: 1}); // MockExternalApiFlags.TRUST_CHECK_ACTIVE_TRUSTED_USER });
const cmdPrefix = `curl -iLX POST --data "${mockPayload}" ${host}`;
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)),
]).then(done);
});
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', done => {
const bodyRegex = /^Unknown resource/;
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)),
]).then(done);
});
});
});

View File

@ -1,101 +1,80 @@
// Imports // Imports
import * as path from 'path'; import {AIO_NGINX_HOSTNAME} from '../common/env-variables';
import * as c from './constants'; import {computeShortSha} from '../common/utils';
import {helper as h} from './helper'; import {ALT_SHA, BuildNums, PrNums, SHA} from './constants';
import {helper as h, makeCurl, payload} from './helper';
import {customMatchers} from './jasmine-custom-matchers';
// Tests // Tests
h.runForAllSupportedSchemes((scheme, port) => describe(`integration (on ${scheme.toUpperCase()})`, () => { h.runForAllSupportedSchemes((scheme, port) => describe(`integration (on ${scheme.toUpperCase()})`, () => {
const hostname = h.nginxHostname; const hostname = AIO_NGINX_HOSTNAME;
const host = `${hostname}:${port}`; const host = `${hostname}:${port}`;
const pr9 = '9'; const curlPrUpdated = makeCurl(`${scheme}://${host}/pr-updated`);
const sha9 = '9'.repeat(40);
const sha0 = '0'.repeat(40);
const archivePath = path.join(h.buildsDir, 'snapshot.tar.gz');
const getFile = (pr: string, sha: string, file: string) => const getFile = (pr: number, sha: string, file: string) =>
h.runCmd(`curl -iL ${scheme}://pr${pr}-${h.getShordSha(sha)}.${host}/${file}`); h.runCmd(`curl -iL ${scheme}://pr${pr}-${computeShortSha(sha)}.${host}/${file}`);
const uploadBuild = (pr: string, sha: string, archive: string, authHeader = 'Token FOO') => { const prUpdated = (prNum: number, action?: string) => curlPrUpdated({ data: { number: prNum, action } });
const curlPost = `curl -iLX POST --header "Authorization: ${authHeader}"`; const circleBuild = makeCurl(`${scheme}://${host}/circle-build`);
return h.runCmd(`${curlPost} --data-binary "@${archive}" ${scheme}://${host}/create-build/${pr}/${sha}`);
};
const prUpdated = (pr: number, action?: string) => {
const url = `${scheme}://${host}/pr-updated`;
const payloadStr = JSON.stringify({number: pr, action});
return h.runCmd(`curl -iLX POST --header "Content-Type: application/json" --data '${payloadStr}' ${url}`);
};
beforeEach(() => jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000); beforeEach(() => {
afterEach(() => { jasmine.DEFAULT_TIMEOUT_INTERVAL = 5000;
h.deletePrDir(pr9); jasmine.addMatchers(customMatchers);
h.deletePrDir(pr9, false);
h.cleanUp();
}); });
afterEach(() => h.cleanUp());
describe('for a new/non-existing PR', () => { describe('for a new/non-existing PR', () => {
it('should be able to upload and serve a public build', done => { it('should be able to create and serve a public preview', async () => {
const regexPrefix9 = `^PR: uploaded\\/${pr9} \\| SHA: ${sha9} \\| File:`; const BUILD = BuildNums.TRUST_CHECK_ACTIVE_TRUSTED_USER;
const idxContentRegex9 = new RegExp(`${regexPrefix9} \\/index\\.html$`); const PR = PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER;
const barContentRegex9 = new RegExp(`${regexPrefix9} \\/foo\\/bar\\.js$`);
h.createDummyArchive(pr9, sha9, archivePath); const regexPrefix = `^BUILD: ${BUILD} \\| PR: ${PR} \\| SHA: ${SHA} \\| File:`;
const idxContentRegex = new RegExp(`${regexPrefix} \\/index\\.html$`);
const barContentRegex = new RegExp(`${regexPrefix} \\/foo\\/bar\\.js$`);
uploadBuild(pr9, sha9, archivePath). await circleBuild(payload(BUILD)).then(h.verifyResponse(201));
then(() => Promise.all([ await Promise.all([
getFile(pr9, sha9, 'index.html').then(h.verifyResponse(200, idxContentRegex9)), getFile(PR, SHA, 'index.html').then(h.verifyResponse(200, idxContentRegex)),
getFile(pr9, sha9, 'foo/bar.js').then(h.verifyResponse(200, barContentRegex9)), getFile(PR, SHA, 'foo/bar.js').then(h.verifyResponse(200, barContentRegex)),
])). ]);
then(done);
expect({ prNum: PR }).toExistAsABuild();
expect({ prNum: PR, isPublic: false }).not.toExistAsABuild();
}); });
it('should be able to upload but not serve a hidden build', done => { it('should be able to create but not serve a hidden preview', async () => {
const regexPrefix9 = `^PR: uploaded\\/${pr9} \\| SHA: ${sha9} \\| File:`; const BUILD = BuildNums.TRUST_CHECK_UNTRUSTED;
const idxContentRegex9 = new RegExp(`${regexPrefix9} \\/index\\.html$`); const PR = PrNums.TRUST_CHECK_UNTRUSTED;
const barContentRegex9 = new RegExp(`${regexPrefix9} \\/foo\\/bar\\.js$`);
h.createDummyArchive(pr9, sha9, archivePath); 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)),
]);
uploadBuild(pr9, sha9, archivePath, c.BV_verify_verifiedNotTrusted). expect({ prNum: PR }).not.toExistAsABuild();
then(() => Promise.all([ expect({ prNum: PR, isPublic: false }).toExistAsABuild();
getFile(pr9, sha9, 'index.html').then(h.verifyResponse(404)),
getFile(pr9, sha9, 'foo/bar.js').then(h.verifyResponse(404)),
])).
then(() => {
expect(h.buildExists(pr9, sha9)).toBe(false);
expect(h.buildExists(pr9, sha9, false)).toBe(true);
expect(h.readBuildFile(pr9, sha9, 'index.html', false)).toMatch(idxContentRegex9);
expect(h.readBuildFile(pr9, sha9, 'foo/bar.js', false)).toMatch(barContentRegex9);
}).
then(done);
}); });
it('should reject an upload if verification fails', done => { it('should reject if verification fails', async () => {
const errorRegex9 = new RegExp(`Error while verifying upload for PR ${pr9}: Test`); const BUILD = BuildNums.TRUST_CHECK_ERROR;
const PR = PrNums.TRUST_CHECK_ERROR;
h.createDummyArchive(pr9, sha9, archivePath); await circleBuild(payload(BUILD)).then(h.verifyResponse(500));
expect({ prNum: PR }).toExistAsAnArtifact();
uploadBuild(pr9, sha9, archivePath, c.BV_verify_error). expect({ prNum: PR }).not.toExistAsABuild();
then(h.verifyResponse(403, errorRegex9)). expect({ prNum: PR, isPublic: false }).not.toExistAsABuild();
then(() => {
expect(h.buildExists(pr9)).toBe(false);
expect(h.buildExists(pr9, '', false)).toBe(false);
}).
then(done);
}); });
it('should be able to notify that a PR has been updated (and do nothing)', done => { it('should be able to notify that a PR has been updated (and do nothing)', async () => {
prUpdated(+pr9). await prUpdated(PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER).then(h.verifyResponse(200));
then(h.verifyResponse(200)).
then(() => {
// The PR should still not exist. // The PR should still not exist.
expect(h.buildExists(pr9, '', false)).toBe(false); expect({ prNum: PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER, isPublic: false }).not.toExistAsABuild();
expect(h.buildExists(pr9, '', true)).toBe(false); expect({ prNum: PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER, isPublic: true }).not.toExistAsABuild();
}).
then(done);
}); });
}); });
@ -103,215 +82,186 @@ h.runForAllSupportedSchemes((scheme, port) => describe(`integration (on ${scheme
describe('for an existing PR', () => { describe('for an existing PR', () => {
it('should be able to upload and serve a public build', done => { it('should be able to create and serve a public preview', async () => {
const regexPrefix0 = `^PR: ${pr9} \\| SHA: ${sha0} \\| File:`; const BUILD = BuildNums.TRUST_CHECK_ACTIVE_TRUSTED_USER;
const idxContentRegex0 = new RegExp(`${regexPrefix0} \\/index\\.html$`); const PR = PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER;
const barContentRegex0 = new RegExp(`${regexPrefix0} \\/foo\\/bar\\.js$`);
const regexPrefix9 = `^PR: uploaded\\/${pr9} \\| SHA: ${sha9} \\| File:`; const regexPrefix1 = `^PR: ${PR} \\| SHA: ${ALT_SHA} \\| File:`;
const idxContentRegex9 = new RegExp(`${regexPrefix9} \\/index\\.html$`); const idxContentRegex1 = new RegExp(`${regexPrefix1} \\/index\\.html$`);
const barContentRegex9 = new RegExp(`${regexPrefix9} \\/foo\\/bar\\.js$`); const barContentRegex1 = new RegExp(`${regexPrefix1} \\/foo\\/bar\\.js$`);
h.createDummyBuild(pr9, sha0); const regexPrefix2 = `^BUILD: ${BUILD} \\| PR: ${PR} \\| SHA: ${SHA} \\| File:`;
h.createDummyArchive(pr9, sha9, archivePath); const idxContentRegex2 = new RegExp(`${regexPrefix2} \\/index\\.html$`);
const barContentRegex2 = new RegExp(`${regexPrefix2} \\/foo\\/bar\\.js$`);
uploadBuild(pr9, sha9, archivePath). h.createDummyBuild(PR, ALT_SHA);
then(() => Promise.all([ await circleBuild(payload(BUILD)).then(h.verifyResponse(201));
getFile(pr9, sha0, 'index.html').then(h.verifyResponse(200, idxContentRegex0)), await Promise.all([
getFile(pr9, sha0, 'foo/bar.js').then(h.verifyResponse(200, barContentRegex0)), getFile(PR, ALT_SHA, 'index.html').then(h.verifyResponse(200, idxContentRegex1)),
getFile(pr9, sha9, 'index.html').then(h.verifyResponse(200, idxContentRegex9)), getFile(PR, ALT_SHA, 'foo/bar.js').then(h.verifyResponse(200, barContentRegex1)),
getFile(pr9, sha9, 'foo/bar.js').then(h.verifyResponse(200, barContentRegex9)), getFile(PR, SHA, 'index.html').then(h.verifyResponse(200, idxContentRegex2)),
])). getFile(PR, SHA, 'foo/bar.js').then(h.verifyResponse(200, barContentRegex2)),
then(done); ]);
expect({ prNum: PR, sha: SHA }).toExistAsABuild();
expect({ prNum: PR, sha: ALT_SHA }).toExistAsABuild();
}); });
it('should be able to upload but not serve a hidden build', done => { it('should be able to create but not serve a hidden preview', async () => {
const regexPrefix0 = `^PR: ${pr9} \\| SHA: ${sha0} \\| File:`; const BUILD = BuildNums.TRUST_CHECK_UNTRUSTED;
const idxContentRegex0 = new RegExp(`${regexPrefix0} \\/index\\.html$`); const PR = PrNums.TRUST_CHECK_UNTRUSTED;
const barContentRegex0 = new RegExp(`${regexPrefix0} \\/foo\\/bar\\.js$`);
const regexPrefix9 = `^PR: uploaded\\/${pr9} \\| SHA: ${sha9} \\| File:`; h.createDummyBuild(PR, ALT_SHA, false);
const idxContentRegex9 = new RegExp(`${regexPrefix9} \\/index\\.html$`); await circleBuild(payload(BUILD)).then(h.verifyResponse(202));
const barContentRegex9 = new RegExp(`${regexPrefix9} \\/foo\\/bar\\.js$`);
h.createDummyBuild(pr9, sha0, false); await Promise.all([
h.createDummyArchive(pr9, sha9, archivePath); 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)),
]);
uploadBuild(pr9, sha9, archivePath, c.BV_verify_verifiedNotTrusted). expect({ prNum: PR, sha: SHA }).not.toExistAsABuild();
then(() => Promise.all([ expect({ prNum: PR, sha: SHA, isPublic: false }).toExistAsABuild();
getFile(pr9, sha0, 'index.html').then(h.verifyResponse(404)), expect({ prNum: PR, sha: ALT_SHA }).not.toExistAsABuild();
getFile(pr9, sha0, 'foo/bar.js').then(h.verifyResponse(404)), expect({ prNum: PR, sha: ALT_SHA, isPublic: false }).toExistAsABuild();
getFile(pr9, sha9, 'index.html').then(h.verifyResponse(404)),
getFile(pr9, sha9, 'foo/bar.js').then(h.verifyResponse(404)),
])).
then(() => {
expect(h.buildExists(pr9, sha9)).toBe(false);
expect(h.buildExists(pr9, sha9, false)).toBe(true);
expect(h.readBuildFile(pr9, sha0, 'index.html', false)).toMatch(idxContentRegex0);
expect(h.readBuildFile(pr9, sha0, 'foo/bar.js', false)).toMatch(barContentRegex0);
expect(h.readBuildFile(pr9, sha9, 'index.html', false)).toMatch(idxContentRegex9);
expect(h.readBuildFile(pr9, sha9, 'foo/bar.js', false)).toMatch(barContentRegex9);
}).
then(done);
}); });
it('should reject an upload if verification fails', done => { it('should reject if verification fails', async () => {
const errorRegex9 = new RegExp(`Error while verifying upload for PR ${pr9}: Test`); const BUILD = BuildNums.TRUST_CHECK_ERROR;
const PR = PrNums.TRUST_CHECK_ERROR;
h.createDummyBuild(pr9, sha0); h.createDummyBuild(PR, ALT_SHA, false);
h.createDummyArchive(pr9, sha9, archivePath);
uploadBuild(pr9, sha9, archivePath, c.BV_verify_error). await circleBuild(payload(BUILD)).then(h.verifyResponse(500));
then(h.verifyResponse(403, errorRegex9)).
then(() => {
expect(h.buildExists(pr9)).toBe(true);
expect(h.buildExists(pr9, sha0)).toBe(true);
expect(h.buildExists(pr9, sha9)).toBe(false);
}).
then(done);
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 build', done => { it('should not be able to overwrite an existing public preview', async () => {
const regexPrefix9 = `^PR: ${pr9} \\| SHA: ${sha9} \\| File:`; const BUILD = BuildNums.TRUST_CHECK_ACTIVE_TRUSTED_USER;
const idxContentRegex9 = new RegExp(`${regexPrefix9} \\/index\\.html$`); const PR = PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER;
const barContentRegex9 = new RegExp(`${regexPrefix9} \\/foo\\/bar\\.js$`);
h.createDummyBuild(pr9, sha9); const regexPrefix = `^PR: ${PR} \\| SHA: ${SHA} \\| File:`;
h.createDummyArchive(pr9, sha9, archivePath); const idxContentRegex = new RegExp(`${regexPrefix} \\/index\\.html$`);
const barContentRegex = new RegExp(`${regexPrefix} \\/foo\\/bar\\.js$`);
uploadBuild(pr9, sha9, archivePath). h.createDummyBuild(PR, SHA);
then(h.verifyResponse(409)).
then(() => Promise.all([ await circleBuild(payload(BUILD)).then(h.verifyResponse(409));
getFile(pr9, sha9, 'index.html').then(h.verifyResponse(200, idxContentRegex9)), await Promise.all([
getFile(pr9, sha9, 'foo/bar.js').then(h.verifyResponse(200, barContentRegex9)), 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)),
then(done); ]);
expect({ prNum: PR }).toExistAsAnArtifact();
expect({ prNum: PR }).toExistAsABuild();
}); });
it('should not be able to overwrite an existing hidden build', done => { it('should not be able to overwrite an existing hidden preview', async () => {
const regexPrefix9 = `^PR: ${pr9} \\| SHA: ${sha9} \\| File:`; const BUILD = BuildNums.TRUST_CHECK_UNTRUSTED;
const idxContentRegex9 = new RegExp(`${regexPrefix9} \\/index\\.html$`); const PR = PrNums.TRUST_CHECK_UNTRUSTED;
const barContentRegex9 = new RegExp(`${regexPrefix9} \\/foo\\/bar\\.js$`); h.createDummyBuild(PR, SHA, false);
h.createDummyBuild(pr9, sha9, false); await circleBuild(payload(BUILD)).then(h.verifyResponse(409));
h.createDummyArchive(pr9, sha9, archivePath);
uploadBuild(pr9, sha9, archivePath, c.BV_verify_verifiedNotTrusted). expect({ prNum: PR }).toExistAsAnArtifact();
then(h.verifyResponse(409)). expect({ prNum: PR, isPublic: false }).toExistAsABuild();
then(() => {
expect(h.readBuildFile(pr9, sha9, 'index.html', false)).toMatch(idxContentRegex9);
expect(h.readBuildFile(pr9, sha9, 'foo/bar.js', false)).toMatch(barContentRegex9);
}).
then(done);
}); });
it('should be able to request re-checking visibility (if outdated)', done => { it('should be able to request re-checking visibility (if outdated)', async () => {
const publicPr = pr9; const publicPr = PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER;
const hiddenPr = String(c.BV_getPrIsTrusted_notTrusted); const hiddenPr = PrNums.TRUST_CHECK_UNTRUSTED;
h.createDummyBuild(publicPr, sha9, false); h.createDummyBuild(publicPr, SHA, false);
h.createDummyBuild(hiddenPr, sha9, true); h.createDummyBuild(hiddenPr, SHA, true);
// PR visibilities are outdated (i.e. the opposte of what the should). // PR visibilities are outdated (i.e. the opposte of what the should).
expect(h.buildExists(publicPr, '', false)).toBe(true); expect({ prNum: publicPr, sha: SHA, isPublic: false }).toExistAsABuild(false);
expect(h.buildExists(publicPr, '', true)).toBe(false); expect({ prNum: publicPr, sha: SHA, isPublic: true }).not.toExistAsABuild(false);
expect(h.buildExists(hiddenPr, '', false)).toBe(false); expect({ prNum: hiddenPr, sha: SHA, isPublic: false }).not.toExistAsABuild(false);
expect(h.buildExists(hiddenPr, '', true)).toBe(true); expect({ prNum: hiddenPr, sha: SHA, isPublic: true }).toExistAsABuild(false);
await Promise.all([
prUpdated(publicPr).then(h.verifyResponse(200)),
prUpdated(hiddenPr).then(h.verifyResponse(200)),
]);
Promise.
all([
prUpdated(+publicPr).then(h.verifyResponse(200)),
prUpdated(+hiddenPr).then(h.verifyResponse(200)),
]).
then(() => {
// PR visibilities should have been updated. // PR visibilities should have been updated.
expect(h.buildExists(publicPr, '', false)).toBe(false); expect({ prNum: publicPr, isPublic: false }).not.toExistAsABuild();
expect(h.buildExists(publicPr, '', true)).toBe(true); expect({ prNum: publicPr, isPublic: true }).toExistAsABuild();
expect(h.buildExists(hiddenPr, '', false)).toBe(true); expect({ prNum: hiddenPr, isPublic: false }).toExistAsABuild();
expect(h.buildExists(hiddenPr, '', true)).toBe(false); expect({ prNum: hiddenPr, isPublic: true }).not.toExistAsABuild();
}).
then(() => {
h.deletePrDir(publicPr, true);
h.deletePrDir(hiddenPr, false);
}).
then(done);
}); });
it('should be able to request re-checking visibility (if up-to-date)', done => { it('should be able to request re-checking visibility (if up-to-date)', async () => {
const publicPr = pr9; const publicPr = PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER;
const hiddenPr = String(c.BV_getPrIsTrusted_notTrusted); const hiddenPr = PrNums.TRUST_CHECK_UNTRUSTED;
h.createDummyBuild(publicPr, sha9, true); h.createDummyBuild(publicPr, SHA, true);
h.createDummyBuild(hiddenPr, sha9, false); h.createDummyBuild(hiddenPr, SHA, false);
// PR visibilities are already up-to-date. // PR visibilities are already up-to-date.
expect(h.buildExists(publicPr, '', false)).toBe(false); expect({ prNum: publicPr, sha: SHA, isPublic: false }).not.toExistAsABuild(false);
expect(h.buildExists(publicPr, '', true)).toBe(true); expect({ prNum: publicPr, sha: SHA, isPublic: true }).toExistAsABuild(false);
expect(h.buildExists(hiddenPr, '', false)).toBe(true); expect({ prNum: hiddenPr, sha: SHA, isPublic: false }).toExistAsABuild(false);
expect(h.buildExists(hiddenPr, '', true)).toBe(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)),
]);
Promise.
all([
prUpdated(+publicPr).then(h.verifyResponse(200)),
prUpdated(+hiddenPr).then(h.verifyResponse(200)),
]).
then(() => {
// PR visibilities are still up-to-date. // PR visibilities are still up-to-date.
expect(h.buildExists(publicPr, '', false)).toBe(false); expect({ prNum: publicPr, isPublic: true }).toExistAsABuild();
expect(h.buildExists(publicPr, '', true)).toBe(true); expect({ prNum: publicPr, isPublic: false }).not.toExistAsABuild();
expect(h.buildExists(hiddenPr, '', false)).toBe(true); expect({ prNum: hiddenPr, isPublic: true }).not.toExistAsABuild();
expect(h.buildExists(hiddenPr, '', true)).toBe(false); expect({ prNum: hiddenPr, isPublic: false }).toExistAsABuild();
}).
then(done);
}); });
it('should reject a request if re-checking visibility fails', done => { it('should reject a request if re-checking visibility fails', async () => {
const errorPr = String(c.BV_getPrIsTrusted_error); const errorPr = PrNums.TRUST_CHECK_ERROR;
h.createDummyBuild(errorPr, sha9, true); h.createDummyBuild(errorPr, SHA, true);
expect(h.buildExists(errorPr, '', false)).toBe(false); expect({ prNum: errorPr, isPublic: false }).not.toExistAsABuild(false);
expect(h.buildExists(errorPr, '', true)).toBe(true); expect({ prNum: errorPr, isPublic: true }).toExistAsABuild(false);
await prUpdated(errorPr).then(h.verifyResponse(500, /TRUST_CHECK_ERROR/));
prUpdated(+errorPr).
then(h.verifyResponse(500, /Test/)).
then(() => {
// PR visibility should not have been updated. // PR visibility should not have been updated.
expect(h.buildExists(errorPr, '', false)).toBe(false); expect({ prNum: errorPr, isPublic: false }).not.toExistAsABuild();
expect(h.buildExists(errorPr, '', true)).toBe(true); expect({ prNum: errorPr, isPublic: true }).toExistAsABuild();
}).
then(done);
}); });
it('should reject a request if updating visibility fails', done => { 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. // One way to cause an error is to have both a public and a hidden directory for the same PR.
h.createDummyBuild(pr9, sha9, false); h.createDummyBuild(PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER, SHA, false);
h.createDummyBuild(pr9, sha9, true); h.createDummyBuild(PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER, SHA, true);
const hiddenPrDir = h.getPrDir(pr9, false); const hiddenPrDir = h.getPrDir(PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER, false);
const publicPrDir = h.getPrDir(pr9, true); const publicPrDir = h.getPrDir(PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER, true);
const bodyRegex = new RegExp(`Request to move '${hiddenPrDir}' to existing directory '${publicPrDir}'`); const bodyRegex = new RegExp(`Request to move '${hiddenPrDir}' to existing directory '${publicPrDir}'`);
expect(h.buildExists(pr9, '', false)).toBe(true); expect({ prNum: PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER, isPublic: false }).toExistAsABuild(false);
expect(h.buildExists(pr9, '', true)).toBe(true); 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));
prUpdated(+pr9).
then(h.verifyResponse(409, bodyRegex)).
then(() => {
// PR visibility should not have been updated. // PR visibility should not have been updated.
expect(h.buildExists(pr9, '', false)).toBe(true); expect({ prNum: PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER, isPublic: false }).toExistAsABuild();
expect(h.buildExists(pr9, '', true)).toBe(true); expect({ prNum: PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER, isPublic: true }).toExistAsABuild();
}).
then(done);
}); });
}); });

View File

@ -0,0 +1,2 @@
import '../preview-server';
import './mock-external-apis';

View File

@ -1,38 +0,0 @@
// Imports
import {GithubPullRequests} from '../common/github-pull-requests';
import {BUILD_VERIFICATION_STATUS, BuildVerifier} from '../upload-server/build-verifier';
import {UploadError} from '../upload-server/upload-error';
import * as c from './constants';
// Run
// TODO(gkalpak): Add e2e tests to cover these interactions as well.
GithubPullRequests.prototype.addComment = () => Promise.resolve();
BuildVerifier.prototype.getPrIsTrusted = (pr: number) => {
switch (pr) {
case c.BV_getPrIsTrusted_error:
// For e2e tests, fake an error.
return Promise.reject('Test');
case c.BV_getPrIsTrusted_notTrusted:
// For e2e tests, fake an untrusted PR (`false`).
return Promise.resolve(false);
default:
// For e2e tests, default to trusted PRs (`true`).
return Promise.resolve(true);
}
};
BuildVerifier.prototype.verify = (expectedPr: number, authHeader: string) => {
switch (authHeader) {
case c.BV_verify_error:
// For e2e tests, fake a verification error.
return Promise.reject(new UploadError(403, `Error while verifying upload for PR ${expectedPr}: Test`));
case c.BV_verify_verifiedNotTrusted:
// For e2e tests, fake a `verifiedNotTrusted` verification status.
return Promise.resolve(BUILD_VERIFICATION_STATUS.verifiedNotTrusted);
default:
// For e2e tests, default to `verifiedAndTrusted` verification status.
return Promise.resolve(BUILD_VERIFICATION_STATUS.verifiedAndTrusted);
}
};
// tslint:disable-next-line: no-var-requires
require('../upload-server/index');

View File

@ -0,0 +1,30 @@
declare module 'tar-stream' {
import {Readable, Writable} from 'stream';
export interface Pack extends Readable {
entry(header: Header, callback?: (err?: any) => {}): Writable;
entry(header: Header, contents: string, callback?: (err?: any) => {}): Writable;
entry(header: Header, buffer: Buffer, callback?: (err?: any) => {}): Writable;
entry(header: Header, buffer: string|Buffer, callback?: (err?: any) => {}): Writable;
finalize();
destroy(err: any);
}
export interface Header {
name: string;
mode?: number;
uid?: number;
gid?: number;
size?: number;
mtime?: Date;
type?: type;
linkname?: string;
uname?: string;
gname?: string;
devmajor?: number;
devminor?: number;
}
export function pack(): Pack;
}

View File

@ -1,571 +0,0 @@
// Imports
import * as fs from 'fs';
import * as path from 'path';
import * as c from './constants';
import {CmdResult, helper as h} from './helper';
// Tests
describe('upload-server (on HTTP)', () => {
const hostname = h.uploadHostname;
const port = h.uploadPort;
const host = `${hostname}:${port}`;
const pr = '9';
const sha9 = '9'.repeat(40);
const sha0 = '0'.repeat(40);
beforeEach(() => jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000);
afterEach(() => h.cleanUp());
describe(`${host}/create-build/<pr>/<sha>`, () => {
const authorizationHeader = `--header "Authorization: Token FOO"`;
const xFileHeader = `--header "X-File: ${h.buildsDir}/snapshot.tar.gz"`;
const defaultHeaders = `${authorizationHeader} ${xFileHeader}`;
const curl = (url: string, headers = defaultHeaders) => `curl -iL ${headers} ${url}`;
it('should disallow non-GET requests', done => {
const url = `http://${host}/create-build/${pr}/${sha9}`;
const bodyRegex = /^Unknown resource/;
Promise.all([
h.runCmd(`curl -iLX PUT ${url}`).then(h.verifyResponse(404, bodyRegex)),
h.runCmd(`curl -iLX POST ${url}`).then(h.verifyResponse(404, bodyRegex)),
h.runCmd(`curl -iLX PATCH ${url}`).then(h.verifyResponse(404, bodyRegex)),
h.runCmd(`curl -iLX DELETE ${url}`).then(h.verifyResponse(404, bodyRegex)),
]).then(done);
});
it('should reject requests without an \'AUTHORIZATION\' header', done => {
const headers1 = '';
const headers2 = '--header "AUTHORIXATION: "';
const url = `http://${host}/create-build/${pr}/${sha9}`;
const bodyRegex = /^Missing or empty 'AUTHORIZATION' header/;
Promise.all([
h.runCmd(curl(url, headers1)).then(h.verifyResponse(401, bodyRegex)),
h.runCmd(curl(url, headers2)).then(h.verifyResponse(401, bodyRegex)),
]).then(done);
});
it('should reject requests without an \'X-FILE\' header', done => {
const headers1 = authorizationHeader;
const headers2 = `${authorizationHeader} --header "X-FILE: "`;
const url = `http://${host}/create-build/${pr}/${sha9}`;
const bodyRegex = /^Missing or empty 'X-FILE' header/;
Promise.all([
h.runCmd(curl(url, headers1)).then(h.verifyResponse(400, bodyRegex)),
h.runCmd(curl(url, headers2)).then(h.verifyResponse(400, bodyRegex)),
]).then(done);
});
it('should reject requests for which the PR verification fails', done => {
const headers = `--header "Authorization: ${c.BV_verify_error}" ${xFileHeader}`;
const url = `http://${host}/create-build/${pr}/${sha9}`;
const bodyRegex = new RegExp(`Error while verifying upload for PR ${pr}: Test`);
h.runCmd(curl(url, headers)).
then(h.verifyResponse(403, bodyRegex)).
then(done);
});
it('should respond with 404 for unknown paths', done => {
const cmdPrefix = curl(`http://${host}`);
Promise.all([
h.runCmd(`${cmdPrefix}/foo/create-build/${pr}/${sha9}`).then(h.verifyResponse(404)),
h.runCmd(`${cmdPrefix}/foo-create-build/${pr}/${sha9}`).then(h.verifyResponse(404)),
h.runCmd(`${cmdPrefix}/fooncreate-build/${pr}/${sha9}`).then(h.verifyResponse(404)),
h.runCmd(`${cmdPrefix}/create-build/foo/${pr}/${sha9}`).then(h.verifyResponse(404)),
h.runCmd(`${cmdPrefix}/create-build-foo/${pr}/${sha9}`).then(h.verifyResponse(404)),
h.runCmd(`${cmdPrefix}/create-buildnfoo/${pr}/${sha9}`).then(h.verifyResponse(404)),
h.runCmd(`${cmdPrefix}/create-build/pr${pr}/${sha9}`).then(h.verifyResponse(404)),
h.runCmd(`${cmdPrefix}/create-build/${pr}/${sha9}42`).then(h.verifyResponse(404)),
]).then(done);
});
it('should reject PRs with leading zeros', done => {
h.runCmd(curl(`http://${host}/create-build/0${pr}/${sha9}`)).
then(h.verifyResponse(404)).
then(done);
});
it('should accept SHAs with leading zeros (but not trim the zeros)', done => {
Promise.all([
h.runCmd(curl(`http://${host}/create-build/${pr}/0${sha9}`)).then(h.verifyResponse(404)),
h.runCmd(curl(`http://${host}/create-build/${pr}/${sha9}`)).then(h.verifyResponse(500)),
h.runCmd(curl(`http://${host}/create-build/${pr}/${sha0}`)).then(h.verifyResponse(500)),
]).then(done);
});
[true, false].forEach(isPublic => describe(`(for ${isPublic ? 'public' : 'hidden'} builds)`, () => {
const authorizationHeader2 = isPublic ?
authorizationHeader : `--header "Authorization: ${c.BV_verify_verifiedNotTrusted}"`;
const cmdPrefix = curl('', `${authorizationHeader2} ${xFileHeader}`);
const overwriteRe = RegExp(`^Request to overwrite existing ${isPublic ? 'public' : 'non-public'} directory`);
it('should not overwrite existing builds', done => {
h.createDummyBuild(pr, sha9, isPublic);
expect(h.readBuildFile(pr, sha9, 'index.html', isPublic)).toContain('index.html');
h.writeBuildFile(pr, sha9, 'index.html', 'My content', isPublic);
expect(h.readBuildFile(pr, sha9, 'index.html', isPublic)).toBe('My content');
h.runCmd(`${cmdPrefix} http://${host}/create-build/${pr}/${sha9}`).
then(h.verifyResponse(409, overwriteRe)).
then(() => expect(h.readBuildFile(pr, sha9, 'index.html', isPublic)).toBe('My content')).
then(done);
});
it('should not overwrite existing builds (even if the SHA is different)', done => {
// 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.
const sha9Almost = sha9.replace(/.$/, '8');
expect(sha9Almost).not.toBe(sha9);
h.createDummyBuild(pr, sha9, isPublic);
expect(h.readBuildFile(pr, sha9, 'index.html', isPublic)).toContain('index.html');
h.writeBuildFile(pr, sha9, 'index.html', 'My content', isPublic);
expect(h.readBuildFile(pr, sha9, 'index.html', isPublic)).toBe('My content');
h.runCmd(`${cmdPrefix} http://${host}/create-build/${pr}/${sha9Almost}`).
then(h.verifyResponse(409, overwriteRe)).
then(() => expect(h.readBuildFile(pr, sha9, 'index.html', isPublic)).toBe('My content')).
then(done);
});
it('should delete the PR directory on error (for new PR)', done => {
h.runCmd(`${cmdPrefix} http://${host}/create-build/${pr}/${sha9}`).
then(h.verifyResponse(500)).
then(() => expect(h.buildExists(pr, '', isPublic)).toBe(false)).
then(done);
});
it('should only delete the SHA directory on error (for existing PR)', done => {
h.createDummyBuild(pr, sha0, isPublic);
h.runCmd(`${cmdPrefix} http://${host}/create-build/${pr}/${sha9}`).
then(h.verifyResponse(500)).
then(() => {
expect(h.buildExists(pr, sha9, isPublic)).toBe(false);
expect(h.buildExists(pr, '', isPublic)).toBe(true);
}).
then(done);
});
describe('on successful upload', () => {
const archivePath = path.join(h.buildsDir, 'snapshot.tar.gz');
const statusCode = isPublic ? 201 : 202;
let uploadPromise: Promise<CmdResult>;
beforeEach(() => {
h.createDummyArchive(pr, sha9, archivePath);
uploadPromise = h.runCmd(`${cmdPrefix} http://${host}/create-build/${pr}/${sha9}`);
});
afterEach(() => h.deletePrDir(pr, isPublic));
it(`should respond with ${statusCode}`, done => {
uploadPromise.then(h.verifyResponse(statusCode)).then(done);
});
it('should extract the contents of the uploaded file', done => {
uploadPromise.
then(() => {
expect(h.readBuildFile(pr, sha9, 'index.html', isPublic)).toContain(`uploaded/${pr}`);
expect(h.readBuildFile(pr, sha9, 'foo/bar.js', isPublic)).toContain(`uploaded/${pr}`);
}).
then(done);
});
it(`should create files/directories owned by '${h.wwwUser}'`, done => {
const prDir = h.getPrDir(pr, isPublic);
const shaDir = h.getShaDir(prDir, sha9);
const idxPath = path.join(shaDir, 'index.html');
const barPath = path.join(shaDir, 'foo', 'bar.js');
uploadPromise.
then(() => Promise.all([
h.runCmd(`find ${shaDir}`),
h.runCmd(`find ${shaDir} -user ${h.wwwUser}`),
])).
then(([{stdout: allFiles}, {stdout: userFiles}]) => {
expect(userFiles).toBe(allFiles);
expect(userFiles).toContain(shaDir);
expect(userFiles).toContain(idxPath);
expect(userFiles).toContain(barPath);
}).
then(done);
});
it('should delete the uploaded file', done => {
expect(fs.existsSync(archivePath)).toBe(true);
uploadPromise.
then(() => expect(fs.existsSync(archivePath)).toBe(false)).
then(done);
});
it('should make the build directory non-writable', done => {
const prDir = h.getPrDir(pr, isPublic);
const shaDir = h.getShaDir(prDir, sha9);
const idxPath = path.join(shaDir, 'index.html');
const barPath = path.join(shaDir, 'foo', 'bar.js');
// 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));
};
uploadPromise.
then(() => {
expect(isNotWritable(shaDir)).toBe(true);
expect(isNotWritable(idxPath)).toBe(true);
expect(isNotWritable(barPath)).toBe(true);
}).
then(done);
});
it('should ignore a legacy 40-chars long build directory (even if it starts with the same chars)', done => {
// 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(pr, sha9, isPublic, false, true);
expect(h.readBuildFile(pr, sha9, 'index.html', isPublic, true)).toContain('index.html');
h.writeBuildFile(pr, sha9, 'index.html', 'My content', isPublic, true);
expect(h.readBuildFile(pr, sha9, 'index.html', isPublic, true)).toBe('My content');
h.runCmd(`${cmdPrefix} http://${host}/create-build/${pr}/${sha9}`).
then(h.verifyResponse(statusCode)).
then(() => {
expect(h.buildExists(pr, sha9, isPublic)).toBe(true);
expect(h.buildExists(pr, sha9, isPublic, true)).toBe(true);
expect(h.readBuildFile(pr, sha9, 'index.html', isPublic)).toContain('index.html');
expect(h.readBuildFile(pr, sha9, 'index.html', isPublic, true)).toBe('My content');
}).
then(done);
});
});
describe('when the PR\'s visibility has changed', () => {
const archivePath = path.join(h.buildsDir, 'snapshot.tar.gz');
const statusCode = isPublic ? 201 : 202;
const checkPrVisibility = (isPublic2: boolean) => {
expect(h.buildExists(pr, '', isPublic2)).toBe(true);
expect(h.buildExists(pr, '', !isPublic2)).toBe(false);
expect(h.buildExists(pr, sha0, isPublic2)).toBe(true);
expect(h.buildExists(pr, sha0, !isPublic2)).toBe(false);
};
const uploadBuild = (sha: string) => h.runCmd(`${cmdPrefix} http://${host}/create-build/${pr}/${sha}`);
beforeEach(() => {
h.createDummyBuild(pr, sha0, !isPublic);
h.createDummyArchive(pr, sha9, archivePath);
checkPrVisibility(!isPublic);
});
afterEach(() => h.deletePrDir(pr, isPublic));
it('should update the PR\'s visibility', done => {
uploadBuild(sha9).
then(h.verifyResponse(statusCode)).
then(() => {
checkPrVisibility(isPublic);
expect(h.buildExists(pr, sha9, isPublic)).toBe(true);
expect(h.readBuildFile(pr, sha9, 'index.html', isPublic)).toContain(`uploaded/${pr}`);
expect(h.readBuildFile(pr, sha9, 'index.html', isPublic)).toContain(sha9);
}).
then(done);
});
it('should not overwrite existing builds (but keep the updated visibility)', done => {
expect(h.buildExists(pr, sha0, isPublic)).toBe(false);
uploadBuild(sha0).
then(h.verifyResponse(409, overwriteRe)).
then(() => {
checkPrVisibility(isPublic);
expect(h.readBuildFile(pr, sha0, 'index.html', isPublic)).toContain(pr);
expect(h.readBuildFile(pr, sha0, 'index.html', isPublic)).not.toContain(`uploaded/${pr}`);
expect(h.readBuildFile(pr, sha0, 'index.html', isPublic)).toContain(sha0);
expect(h.readBuildFile(pr, sha0, 'index.html', isPublic)).not.toContain(sha9);
}).
then(done);
});
it('should reject the request if it fails to update the PR\'s visibility', done => {
// One way to cause an error is to have both a public and a hidden directory for the same PR.
h.createDummyBuild(pr, sha0, isPublic);
expect(h.buildExists(pr, sha0, isPublic)).toBe(true);
expect(h.buildExists(pr, sha0, !isPublic)).toBe(true);
const errorRegex = new RegExp(`^Request to move '${h.getPrDir(pr, !isPublic)}' ` +
`to existing directory '${h.getPrDir(pr, isPublic)}'.`);
uploadBuild(sha9).
then(h.verifyResponse(409, errorRegex)).
then(() => {
expect(h.buildExists(pr, sha0, isPublic)).toBe(true);
expect(h.buildExists(pr, sha0, !isPublic)).toBe(true);
expect(h.buildExists(pr, sha9, isPublic)).toBe(false);
expect(h.buildExists(pr, sha9, !isPublic)).toBe(false);
}).
then(done);
});
});
}));
});
describe(`${host}/health-check`, () => {
it('should respond with 200', done => {
Promise.all([
h.runCmd(`curl -iL http://${host}/health-check`).then(h.verifyResponse(200)),
h.runCmd(`curl -iL http://${host}/health-check/`).then(h.verifyResponse(200)),
]).then(done);
});
it('should respond with 404 if the path does not match exactly', done => {
Promise.all([
h.runCmd(`curl -iL http://${host}/health-check/foo`).then(h.verifyResponse(404)),
h.runCmd(`curl -iL http://${host}/health-check-foo`).then(h.verifyResponse(404)),
h.runCmd(`curl -iL http://${host}/health-checknfoo`).then(h.verifyResponse(404)),
h.runCmd(`curl -iL http://${host}/foo/health-check`).then(h.verifyResponse(404)),
h.runCmd(`curl -iL http://${host}/foo-health-check`).then(h.verifyResponse(404)),
h.runCmd(`curl -iL http://${host}/foonhealth-check`).then(h.verifyResponse(404)),
]).then(done);
});
});
describe(`${host}/pr-updated`, () => {
const url = `http://${host}/pr-updated`;
// Helpers
const curl = (payload?: {number: number, action?: string}) => {
const payloadStr = payload && JSON.stringify(payload) || '';
return `curl -iLX POST --header "Content-Type: application/json" --data '${payloadStr}' ${url}`;
};
it('should disallow non-POST requests', done => {
const bodyRegex = /^Unknown resource in request/;
Promise.all([
h.runCmd(`curl -iLX GET ${url}`).then(h.verifyResponse(404, bodyRegex)),
h.runCmd(`curl -iLX PUT ${url}`).then(h.verifyResponse(404, bodyRegex)),
h.runCmd(`curl -iLX PATCH ${url}`).then(h.verifyResponse(404, bodyRegex)),
h.runCmd(`curl -iLX DELETE ${url}`).then(h.verifyResponse(404, bodyRegex)),
]).then(done);
});
it('should respond with 400 for requests without a payload', done => {
const bodyRegex = /^Missing or empty 'number' field in request/;
h.runCmd(curl()).
then(h.verifyResponse(400, bodyRegex)).
then(done);
});
it('should respond with 400 for requests without a \'number\' field', done => {
const bodyRegex = /^Missing or empty 'number' field in request/;
Promise.all([
h.runCmd(curl({} as any)).then(h.verifyResponse(400, bodyRegex)),
h.runCmd(curl({number: null} as any)).then(h.verifyResponse(400, bodyRegex)),
]).then(done);
});
it('should reject requests for which checking the PR visibility fails', done => {
h.runCmd(curl({number: c.BV_getPrIsTrusted_error})).
then(h.verifyResponse(500, /Test/)).
then(done);
});
it('should respond with 404 for unknown paths', done => {
const mockPayload = JSON.stringify({number: +pr});
const cmdPrefix = `curl -iLX POST --data "${mockPayload}" http://${host}`;
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)),
]).then(done);
});
it('should do nothing if PR\'s visibility is already up-to-date', done => {
const publicPr = pr;
const hiddenPr = String(c.BV_getPrIsTrusted_notTrusted);
const checkVisibilities = () => {
// Public build is already public.
expect(h.buildExists(publicPr, '', false)).toBe(false);
expect(h.buildExists(publicPr, '', true)).toBe(true);
// Hidden build is already hidden.
expect(h.buildExists(hiddenPr, '', false)).toBe(true);
expect(h.buildExists(hiddenPr, '', true)).toBe(false);
};
h.createDummyBuild(publicPr, sha9, true);
h.createDummyBuild(hiddenPr, sha9, false);
checkVisibilities();
Promise.
all([
h.runCmd(curl({number: +publicPr, action: 'foo'})).then(h.verifyResponse(200)),
h.runCmd(curl({number: +hiddenPr, action: 'foo'})).then(h.verifyResponse(200)),
]).
// Visibilities should not have changed, because the specified action could not have triggered a change.
then(checkVisibilities).
then(done);
});
it('should do nothing if \'action\' implies no visibility change', done => {
const publicPr = pr;
const hiddenPr = String(c.BV_getPrIsTrusted_notTrusted);
const checkVisibilities = () => {
// Public build is hidden atm.
expect(h.buildExists(publicPr, '', false)).toBe(true);
expect(h.buildExists(publicPr, '', true)).toBe(false);
// Hidden build is public atm.
expect(h.buildExists(hiddenPr, '', false)).toBe(false);
expect(h.buildExists(hiddenPr, '', true)).toBe(true);
};
h.createDummyBuild(publicPr, sha9, false);
h.createDummyBuild(hiddenPr, sha9, true);
checkVisibilities();
Promise.
all([
h.runCmd(curl({number: +publicPr, action: 'foo'})).then(h.verifyResponse(200)),
h.runCmd(curl({number: +hiddenPr, action: 'foo'})).then(h.verifyResponse(200)),
]).
// Visibilities should not have changed, because the specified action could not have triggered a change.
then(checkVisibilities).
then(done);
});
describe('when the visiblity has changed', () => {
const publicPr = pr;
const hiddenPr = String(c.BV_getPrIsTrusted_notTrusted);
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, sha9, false);
h.createDummyBuild(hiddenPr, sha9, true);
expect(h.buildExists(publicPr, '', false)).toBe(true);
expect(h.buildExists(publicPr, '', true)).toBe(false);
expect(h.buildExists(hiddenPr, '', false)).toBe(false);
expect(h.buildExists(hiddenPr, '', true)).toBe(true);
});
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(h.buildExists(publicPr, '', false)).toBe(false);
expect(h.buildExists(publicPr, '', true)).toBe(true);
expect(h.buildExists(hiddenPr, '', false)).toBe(true);
expect(h.buildExists(hiddenPr, '', true)).toBe(false);
h.deletePrDir(publicPr, true);
h.deletePrDir(hiddenPr, false);
});
it('should update the PR\'s visibility (action: undefined)', done => {
Promise.all([
h.runCmd(curl({number: +publicPr})).then(h.verifyResponse(200)),
h.runCmd(curl({number: +hiddenPr})).then(h.verifyResponse(200)),
]).then(done);
});
it('should update the PR\'s visibility (action: labeled)', done => {
Promise.all([
h.runCmd(curl({number: +publicPr, action: 'labeled'})).then(h.verifyResponse(200)),
h.runCmd(curl({number: +hiddenPr, action: 'labeled'})).then(h.verifyResponse(200)),
]).then(done);
});
it('should update the PR\'s visibility (action: unlabeled)', done => {
Promise.all([
h.runCmd(curl({number: +publicPr, action: 'unlabeled'})).then(h.verifyResponse(200)),
h.runCmd(curl({number: +hiddenPr, action: 'unlabeled'})).then(h.verifyResponse(200)),
]).then(done);
});
});
});
describe(`${host}/*`, () => {
it('should respond with 404 for requests to unknown URLs', done => {
const bodyRegex = /^Unknown resource/;
Promise.all([
h.runCmd(`curl -iL http://${host}/index.html`).then(h.verifyResponse(404, bodyRegex)),
h.runCmd(`curl -iL http://${host}/`).then(h.verifyResponse(404, bodyRegex)),
h.runCmd(`curl -iL http://${host}`).then(h.verifyResponse(404, bodyRegex)),
h.runCmd(`curl -iLX PUT http://${host}`).then(h.verifyResponse(404, bodyRegex)),
h.runCmd(`curl -iLX POST http://${host}`).then(h.verifyResponse(404, bodyRegex)),
h.runCmd(`curl -iLX PATCH http://${host}`).then(h.verifyResponse(404, bodyRegex)),
h.runCmd(`curl -iLX DELETE http://${host}`).then(h.verifyResponse(404, bodyRegex)),
]).then(done);
});
});
});

View File

@ -21,19 +21,23 @@
}, },
"dependencies": { "dependencies": {
"body-parser": "^1.18.2", "body-parser": "^1.18.2",
"delete-empty": "^2.0.0",
"express": "^4.15.4", "express": "^4.15.4",
"jasmine": "^2.8.0", "jasmine": "^2.8.0",
"jsonwebtoken": "^8.0.1", "nock": "^9.2.5",
"shelljs": "^0.7.8", "node-fetch": "^2.1.2",
"shelljs": "^0.8.1",
"tar-stream": "^1.6.0",
"tslib": "^1.7.1" "tslib": "^1.7.1"
}, },
"devDependencies": { "devDependencies": {
"@types/body-parser": "^1.16.5", "@types/body-parser": "^1.16.5",
"@types/express": "^4.0.37", "@types/express": "^4.0.37",
"@types/jasmine": "^2.6.0", "@types/jasmine": "^2.6.0",
"@types/jsonwebtoken": "^7.2.3", "@types/nock": "^9.1.3",
"@types/node": "^8.0.30", "@types/node": "^8.0.30",
"@types/shelljs": "^0.7.4", "@types/node-fetch": "^1.6.8",
"@types/shelljs": "^0.8.0",
"@types/supertest": "^2.0.3", "@types/supertest": "^2.0.3",
"concurrently": "^3.5.0", "concurrently": "^3.5.0",
"nodemon": "^1.12.1", "nodemon": "^1.12.1",

View File

@ -1,135 +1,176 @@
// Imports // Imports
import * as fs from 'fs'; import * as fs from 'fs';
import * as path from 'path'; import {normalize} from 'path';
import * as shell from 'shelljs'; import * as shell from 'shelljs';
import {BuildCleaner} from '../../lib/clean-up/build-cleaner'; import {BuildCleaner} from '../../lib/clean-up/build-cleaner';
import {HIDDEN_DIR_PREFIX} from '../../lib/common/constants'; import {HIDDEN_DIR_PREFIX} from '../../lib/common/constants';
import {GithubPullRequests} from '../../lib/common/github-pull-requests'; import {GithubPullRequests} from '../../lib/common/github-pull-requests';
const EXISTING_BUILDS = [10, 20, 30, 40];
const EXISTING_DOWNLOADS = [
'downloads/10-ABCDEF0-build.zip',
'downloads/10-1234567-build.zip',
'downloads/20-ABCDEF0-build.zip',
'downloads/20-1234567-build.zip',
];
const OPEN_PRS = [10, 40];
const ANY_DATE = jasmine.any(String);
// Tests // Tests
describe('BuildCleaner', () => { describe('BuildCleaner', () => {
let cleaner: BuildCleaner; let cleaner: BuildCleaner;
beforeEach(() => cleaner = new BuildCleaner('/foo/bar', 'baz/qux', '12345')); beforeEach(() => {
spyOn(console, 'error');
spyOn(console, 'log');
cleaner = new BuildCleaner('/foo/bar', 'baz', 'qux', '12345', 'downloads', 'build.zip');
});
describe('constructor()', () => { describe('constructor()', () => {
it('should throw if \'buildsDir\' is empty', () => { it('should throw if \'buildsDir\' is empty', () => {
expect(() => new BuildCleaner('', '/baz/qux', '12345')). expect(() => new BuildCleaner('', 'baz', 'qux', '12345', 'downloads', 'build.zip')).
toThrowError('Missing or empty required parameter \'buildsDir\'!'); toThrowError('Missing or empty required parameter \'buildsDir\'!');
}); });
it('should throw if \'repoSlug\' is empty', () => { it('should throw if \'githubOrg\' is empty', () => {
expect(() => new BuildCleaner('/foo/bar', '', '12345')). expect(() => new BuildCleaner('/foo/bar', '', 'qux', '12345', 'downloads', 'build.zip')).
toThrowError('Missing or empty required parameter \'repoSlug\'!'); 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', () => { it('should throw if \'githubToken\' is empty', () => {
expect(() => new BuildCleaner('/foo/bar', 'baz/qux', '')). expect(() => new BuildCleaner('/foo/bar', 'baz', 'qux', '', 'downloads', 'build.zip')).
toThrowError('Missing or empty required parameter \'githubToken\'!'); 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()', () => { describe('cleanUp()', () => {
let cleanerGetExistingBuildNumbersSpy: jasmine.Spy; let cleanerGetExistingBuildNumbersSpy: jasmine.Spy;
let cleanerGetOpenPrNumbersSpy: jasmine.Spy; let cleanerGetOpenPrNumbersSpy: jasmine.Spy;
let cleanerGetExistingDownloadsSpy: jasmine.Spy;
let cleanerRemoveUnnecessaryBuildsSpy: jasmine.Spy; let cleanerRemoveUnnecessaryBuildsSpy: jasmine.Spy;
let existingBuildsDeferred: {resolve: (v?: any) => void, reject: (e?: any) => void}; let cleanerRemoveUnnecessaryDownloadsSpy: jasmine.Spy;
let openPrsDeferred: {resolve: (v?: any) => void, reject: (e?: any) => void};
let promise: Promise<void>;
beforeEach(() => { beforeEach(() => {
cleanerGetExistingBuildNumbersSpy = spyOn(cleaner as any, 'getExistingBuildNumbers').and.callFake(() => { cleanerGetExistingBuildNumbersSpy = spyOn(cleaner, 'getExistingBuildNumbers')
return new Promise((resolve, reject) => existingBuildsDeferred = {resolve, reject}); .and.callFake(() => Promise.resolve(EXISTING_BUILDS));
}); cleanerGetOpenPrNumbersSpy = spyOn(cleaner, 'getOpenPrNumbers')
cleanerGetOpenPrNumbersSpy = spyOn(cleaner as any, 'getOpenPrNumbers').and.callFake(() => { .and.callFake(() => Promise.resolve(OPEN_PRS));
return new Promise((resolve, reject) => openPrsDeferred = {resolve, reject}); cleanerGetExistingDownloadsSpy = spyOn(cleaner, 'getExistingDownloads')
}); .and.callFake(() => Promise.resolve(EXISTING_DOWNLOADS));
cleanerRemoveUnnecessaryBuildsSpy = spyOn(cleaner as any, 'removeUnnecessaryBuilds');
cleanerRemoveUnnecessaryBuildsSpy = spyOn(cleaner, 'removeUnnecessaryBuilds');
cleanerRemoveUnnecessaryDownloadsSpy = spyOn(cleaner, 'removeUnnecessaryDownloads');
promise = cleaner.cleanUp();
}); });
it('should return a promise', () => { it('should return a promise', () => {
const promise = cleaner.cleanUp();
expect(promise).toEqual(jasmine.any(Promise)); expect(promise).toEqual(jasmine.any(Promise));
}); });
it('should get the existing builds', () => { it('should get the open PRs', async () => {
expect(cleanerGetExistingBuildNumbersSpy).toHaveBeenCalled(); await cleaner.cleanUp();
});
it('should get the open PRs', () => {
expect(cleanerGetOpenPrNumbersSpy).toHaveBeenCalled(); expect(cleanerGetOpenPrNumbersSpy).toHaveBeenCalled();
}); });
it('should reject if \'getExistingBuildNumbers()\' rejects', done => { it('should get the existing builds', async () => {
promise.catch(err => { 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 () => {
try {
cleanerGetOpenPrNumbersSpy.and.callFake(() => Promise.reject('Test'));
await cleaner.cleanUp();
} catch (err) {
expect(err).toBe('Test'); expect(err).toBe('Test');
done(); }
});
existingBuildsDeferred.reject('Test');
}); });
it('should reject if \'getOpenPrNumbers()\' rejects', done => { it('should reject if \'getExistingBuildNumbers()\' rejects', async () => {
promise.catch(err => { try {
cleanerGetExistingBuildNumbersSpy.and.callFake(() => Promise.reject('Test'));
await cleaner.cleanUp();
} catch (err) {
expect(err).toBe('Test'); expect(err).toBe('Test');
done(); }
});
openPrsDeferred.reject('Test');
}); });
it('should reject if \'removeUnnecessaryBuilds()\' rejects', done => { it('should reject if \'getExistingDownloads()\' rejects', async () => {
promise.catch(err => { try {
cleanerGetExistingDownloadsSpy.and.callFake(() => Promise.reject('Test'));
await cleaner.cleanUp();
} catch (err) {
expect(err).toBe('Test'); expect(err).toBe('Test');
done(); }
});
cleanerRemoveUnnecessaryBuildsSpy.and.returnValue(Promise.reject('Test'));
existingBuildsDeferred.resolve();
openPrsDeferred.resolve();
}); });
it('should pass existing builds and open PRs to \'removeUnnecessaryBuilds()\'', done => { it('should reject if \'removeUnnecessaryBuilds()\' rejects', async () => {
promise.then(() => { try {
expect(cleanerRemoveUnnecessaryBuildsSpy).toHaveBeenCalledWith('foo', 'bar'); cleanerRemoveUnnecessaryBuildsSpy.and.callFake(() => Promise.reject('Test'));
done(); await cleaner.cleanUp();
} catch (err) {
expect(err).toBe('Test');
}
}); });
existingBuildsDeferred.resolve('foo'); it('should reject if \'removeUnnecessaryDownloads()\' rejects', async () => {
openPrsDeferred.resolve('bar'); try {
cleanerRemoveUnnecessaryDownloadsSpy.and.callFake(() => Promise.reject('Test'));
await cleaner.cleanUp();
} catch (err) {
expect(err).toBe('Test');
}
});
}); });
it('should resolve with the value returned by \'removeUnnecessaryBuilds()\'', done => {
promise.then(result => {
expect(result as any).toBe('Test');
done();
});
cleanerRemoveUnnecessaryBuildsSpy.and.returnValue(Promise.resolve('Test'));
existingBuildsDeferred.resolve();
openPrsDeferred.resolve();
});
});
// Protected methods
describe('getExistingBuildNumbers()', () => { describe('getExistingBuildNumbers()', () => {
let fsReaddirSpy: jasmine.Spy; let fsReaddirSpy: jasmine.Spy;
let readdirCb: (err: any, files?: string[]) => void; let readdirCb: (err: any, files?: string[]) => void;
@ -137,7 +178,7 @@ describe('BuildCleaner', () => {
beforeEach(() => { beforeEach(() => {
fsReaddirSpy = spyOn(fs, 'readdir').and.callFake((_: string, cb: typeof readdirCb) => readdirCb = cb); fsReaddirSpy = spyOn(fs, 'readdir').and.callFake((_: string, cb: typeof readdirCb) => readdirCb = cb);
promise = (cleaner as any).getExistingBuildNumbers(); promise = cleaner.getExistingBuildNumbers();
}); });
@ -203,7 +244,7 @@ describe('BuildCleaner', () => {
return new Promise((resolve, reject) => prDeferred = {resolve, reject}); return new Promise((resolve, reject) => prDeferred = {resolve, reject});
}); });
promise = (cleaner as any).getOpenPrNumbers(); promise = cleaner.getOpenPrNumbers();
}); });
@ -236,6 +277,65 @@ describe('BuildCleaner', () => {
prDeferred.resolve([{id: 0, number: 1}, {id: 1, number: 2}, {id: 2, number: 3}]); prDeferred.resolve([{id: 0, number: 1}, {id: 1, number: 2}, {id: 2, number: 3}]);
}); });
it('should log the number of open PRs', () => {
promise.then(prNumbers => {
expect(console.log).toHaveBeenCalledWith(ANY_DATE, 'BuildCleaner: ', `Open pull requests: ${prNumbers}`);
});
});
});
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);
promise = cleaner.getExistingDownloads();
});
it('should return a promise', () => {
expect(promise).toEqual(jasmine.any(Promise));
});
it('should get the contents of the builds directory', () => {
expect(fsReaddirSpy).toHaveBeenCalled();
expect(fsReaddirSpy.calls.argsFor(0)[0]).toBe('downloads');
});
it('should reject if an error occurs while getting the files', done => {
promise.catch(err => {
expect(err).toBe('Test');
done();
});
readdirCb('Test');
});
it('should resolve with the returned files (as numbers)', done => {
promise.then(result => {
expect(result).toEqual(EXISTING_DOWNLOADS);
done();
});
readdirCb(null, EXISTING_DOWNLOADS);
});
it('should ignore files that do not match the artifactPath', done => {
promise.then(result => {
expect(result).toEqual(['10-ABCDEF-build.zip', '30-FFFFFFF-build.zip']);
done();
});
readdirCb(null, ['10-ABCDEF-build.zip', '20-AAAAAAA-otherfile.zip', '30-FFFFFFF-build.zip']);
});
}); });
@ -253,7 +353,7 @@ describe('BuildCleaner', () => {
it('should test if the directory exists (and return if is does not)', () => { it('should test if the directory exists (and return if is does not)', () => {
shellTestSpy.and.returnValue(false); shellTestSpy.and.returnValue(false);
(cleaner as any).removeDir('/foo/bar'); cleaner.removeDir('/foo/bar');
expect(shellTestSpy).toHaveBeenCalledWith('-d', '/foo/bar'); expect(shellTestSpy).toHaveBeenCalledWith('-d', '/foo/bar');
expect(shellChmodSpy).not.toHaveBeenCalled(); expect(shellChmodSpy).not.toHaveBeenCalled();
@ -262,99 +362,117 @@ describe('BuildCleaner', () => {
it('should remove the specified directory and its content', () => { it('should remove the specified directory and its content', () => {
(cleaner as any).removeDir('/foo/bar'); cleaner.removeDir('/foo/bar');
expect(shellRmSpy).toHaveBeenCalledWith('-rf', '/foo/bar'); expect(shellRmSpy).toHaveBeenCalledWith('-rf', '/foo/bar');
}); });
it('should make the directory and its content writable before removing', () => { it('should make the directory and its content writable before removing', () => {
shellRmSpy.and.callFake(() => expect(shellChmodSpy).toHaveBeenCalledWith('-R', 'a+w', '/foo/bar')); shellRmSpy.and.callFake(() => expect(shellChmodSpy).toHaveBeenCalledWith('-R', 'a+w', '/foo/bar'));
(cleaner as any).removeDir('/foo/bar'); cleaner.removeDir('/foo/bar');
expect(shellRmSpy).toHaveBeenCalled(); expect(shellRmSpy).toHaveBeenCalled();
}); });
it('should catch errors and log them', () => { it('should catch errors and log them', () => {
const consoleErrorSpy = spyOn(console, 'error');
shellRmSpy.and.callFake(() => { shellRmSpy.and.callFake(() => {
// tslint:disable-next-line: no-string-throw // tslint:disable-next-line: no-string-throw
throw 'Test'; throw 'Test';
}); });
(cleaner as any).removeDir('/foo/bar'); cleaner.removeDir('/foo/bar');
expect(consoleErrorSpy).toHaveBeenCalled(); expect(console.error).toHaveBeenCalledWith(
expect(consoleErrorSpy.calls.argsFor(0)[0]).toContain('Unable to remove \'/foo/bar\''); jasmine.any(String), 'BuildCleaner: ', 'ERROR: Unable to remove \'/foo/bar\' due to:', 'Test');
expect(consoleErrorSpy.calls.argsFor(0)[1]).toBe('Test');
}); });
}); });
describe('removeUnnecessaryBuilds()', () => { describe('removeUnnecessaryBuilds()', () => {
let consoleLogSpy: jasmine.Spy;
let cleanerRemoveDirSpy: jasmine.Spy; let cleanerRemoveDirSpy: jasmine.Spy;
beforeEach(() => { beforeEach(() => {
consoleLogSpy = spyOn(console, 'log'); cleanerRemoveDirSpy = spyOn(cleaner, 'removeDir');
cleanerRemoveDirSpy = spyOn(cleaner as any, 'removeDir');
}); });
it('should log the number of existing builds, open PRs and builds to be removed', () => { it('should log the number of existing builds and builds to be removed', () => {
(cleaner as any).removeUnnecessaryBuilds([1, 2, 3], [3, 4, 5, 6]); cleaner.removeUnnecessaryBuilds([1, 2, 3], [3, 4, 5, 6]);
expect(console.log).toHaveBeenCalledWith('Existing builds: 3'); expect(console.log).toHaveBeenCalledWith(ANY_DATE, 'BuildCleaner: ', 'Existing builds: 3');
expect(console.log).toHaveBeenCalledWith('Open pull requests: 4'); expect(console.log).toHaveBeenCalledWith(ANY_DATE, 'BuildCleaner: ', 'Removing 2 build(s): 1, 2');
expect(console.log).toHaveBeenCalledWith('Removing 2 build(s): 1, 2');
}); });
it('should construct full paths to directories (by prepending \'buildsDir\')', () => { it('should construct full paths to directories (by prepending \'buildsDir\')', () => {
(cleaner as any).removeUnnecessaryBuilds([1, 2, 3], []); cleaner.removeUnnecessaryBuilds([1, 2, 3], []);
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(path.normalize('/foo/bar/1')); expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(normalize('/foo/bar/1'));
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(path.normalize('/foo/bar/2')); expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(normalize('/foo/bar/2'));
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(path.normalize('/foo/bar/3')); expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(normalize('/foo/bar/3'));
}); });
it('should try removing hidden directories as well', () => { it('should try removing hidden directories as well', () => {
(cleaner as any).removeUnnecessaryBuilds([1, 2, 3], []); cleaner.removeUnnecessaryBuilds([1, 2, 3], []);
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(path.normalize(`/foo/bar/${HIDDEN_DIR_PREFIX}1`)); expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(normalize(`/foo/bar/${HIDDEN_DIR_PREFIX}1`));
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(path.normalize(`/foo/bar/${HIDDEN_DIR_PREFIX}2`)); expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(normalize(`/foo/bar/${HIDDEN_DIR_PREFIX}2`));
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(path.normalize(`/foo/bar/${HIDDEN_DIR_PREFIX}3`)); expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(normalize(`/foo/bar/${HIDDEN_DIR_PREFIX}3`));
}); });
it('should remove the builds that do not correspond to open PRs', () => { it('should remove the builds that do not correspond to open PRs', () => {
(cleaner as any).removeUnnecessaryBuilds([1, 2, 3, 4], [2, 4]); cleaner.removeUnnecessaryBuilds([1, 2, 3, 4], [2, 4]);
expect(cleanerRemoveDirSpy).toHaveBeenCalledTimes(4); expect(cleanerRemoveDirSpy).toHaveBeenCalledTimes(4);
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(path.normalize('/foo/bar/1')); expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(normalize('/foo/bar/1'));
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(path.normalize('/foo/bar/3')); expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(normalize('/foo/bar/3'));
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(path.normalize(`/foo/bar/${HIDDEN_DIR_PREFIX}1`)); expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(normalize(`/foo/bar/${HIDDEN_DIR_PREFIX}1`));
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(path.normalize(`/foo/bar/${HIDDEN_DIR_PREFIX}3`)); expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(normalize(`/foo/bar/${HIDDEN_DIR_PREFIX}3`));
cleanerRemoveDirSpy.calls.reset(); cleanerRemoveDirSpy.calls.reset();
(cleaner as any).removeUnnecessaryBuilds([1, 2, 3, 4], [1, 2, 3, 4]); cleaner.removeUnnecessaryBuilds([1, 2, 3, 4], [1, 2, 3, 4]);
expect(cleanerRemoveDirSpy).toHaveBeenCalledTimes(0); expect(cleanerRemoveDirSpy).toHaveBeenCalledTimes(0);
cleanerRemoveDirSpy.calls.reset(); cleanerRemoveDirSpy.calls.reset();
(cleaner as any).removeUnnecessaryBuilds([1, 2, 3, 4], []); (cleaner as any).removeUnnecessaryBuilds([1, 2, 3, 4], []);
expect(cleanerRemoveDirSpy).toHaveBeenCalledTimes(8); expect(cleanerRemoveDirSpy).toHaveBeenCalledTimes(8);
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(path.normalize('/foo/bar/1')); expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(normalize('/foo/bar/1'));
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(path.normalize('/foo/bar/2')); expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(normalize('/foo/bar/2'));
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(path.normalize('/foo/bar/3')); expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(normalize('/foo/bar/3'));
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(path.normalize('/foo/bar/4')); expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(normalize('/foo/bar/4'));
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(path.normalize(`/foo/bar/${HIDDEN_DIR_PREFIX}1`)); expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(normalize(`/foo/bar/${HIDDEN_DIR_PREFIX}1`));
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(path.normalize(`/foo/bar/${HIDDEN_DIR_PREFIX}2`)); expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(normalize(`/foo/bar/${HIDDEN_DIR_PREFIX}2`));
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(path.normalize(`/foo/bar/${HIDDEN_DIR_PREFIX}3`)); expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(normalize(`/foo/bar/${HIDDEN_DIR_PREFIX}3`));
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(path.normalize(`/foo/bar/${HIDDEN_DIR_PREFIX}4`)); expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(normalize(`/foo/bar/${HIDDEN_DIR_PREFIX}4`));
cleanerRemoveDirSpy.calls.reset(); cleanerRemoveDirSpy.calls.reset();
}); });
}); });
describe('removeUnnecessaryDownloads()', () => {
beforeEach(() => {
spyOn(shell, 'rm');
});
it('should remove the downloads that do not correspond to open PRs', () => {
cleaner.removeUnnecessaryDownloads(EXISTING_DOWNLOADS, OPEN_PRS);
expect(shell.rm).toHaveBeenCalledTimes(2);
expect(shell.rm).toHaveBeenCalledWith('downloads/20-ABCDEF0-build.zip');
expect(shell.rm).toHaveBeenCalledWith('downloads/20-1234567-build.zip');
});
it('should log the number of existing builds and builds to be removed', () => {
cleaner.removeUnnecessaryDownloads(EXISTING_DOWNLOADS, OPEN_PRS);
expect(console.log).toHaveBeenCalledWith(ANY_DATE, 'BuildCleaner: ', 'Existing downloads: 4');
expect(console.log).toHaveBeenCalledWith(ANY_DATE, 'BuildCleaner: ',
'Removing 2 download(s): downloads/20-ABCDEF0-build.zip, downloads/20-1234567-build.zip');
});
});
}); });

View File

@ -0,0 +1,134 @@
import * as nock from 'nock';
import {CircleCiApi} from '../../lib/common/circle-ci-api';
const ORG = 'testorg';
const REPO = 'testrepo';
const TOKEN = 'xxxx';
const BASE_URL = `https://circleci.com/api/v1.1/project/github/${ORG}/${REPO}`;
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('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_URL)
.get(`/${buildNum}?circle-token=${TOKEN}`)
.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_URL).get(`/${buildNum}?circle-token=${TOKEN}`);
try {
request.replyWithError(errorMessage);
await api.getBuildInfo(buildNum);
throw new Error('Exception Expected');
} catch (err) {
expect(err.message).toEqual(
`CircleCI build info request failed ` +
`(request to ${BASE_URL}/${buildNum}?circle-token=${TOKEN} failed, reason: ${errorMessage})`);
}
try {
request.reply(404, errorMessage);
await api.getBuildInfo(buildNum);
throw new Error('Exception Expected');
} catch (err) {
expect(err.message).toEqual(
`CircleCI build info request failed ` +
`(request to ${BASE_URL}/${buildNum}?circle-token=${TOKEN} 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_URL)
.get(`/${buildNum}/artifacts?circle-token=${TOKEN}`)
.reply(200, [artifact0, artifact1, artifact2]);
const artifactUrl = await api.getBuildArtifactUrl(buildNum, 'some/path/1');
expect(artifactUrl).toEqual('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_URL).get(`/${buildNum}/artifacts?circle-token=${TOKEN}`);
try {
request.replyWithError(errorMessage);
await api.getBuildArtifactUrl(buildNum, 'some/path/1');
throw new Error('Exception Expected');
} catch (err) {
expect(err.message).toEqual(
`CircleCI artifact URL request failed ` +
`(request to ${BASE_URL}/${buildNum}/artifacts?circle-token=${TOKEN} failed, reason: ${errorMessage})`);
}
try {
request.reply(404, errorMessage);
await api.getBuildArtifactUrl(buildNum, 'some/path/1');
throw new Error('Exception Expected');
} catch (err) {
expect(err.message).toEqual(
`CircleCI artifact URL request failed ` +
`(request to ${BASE_URL}/${buildNum}/artifacts?circle-token=${TOKEN} 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_URL)
.get(`/${buildNum}/artifacts?circle-token=${TOKEN}`)
.reply(200, [artifact0, artifact1, artifact2]);
try {
await api.getBuildArtifactUrl(buildNum, 'some/path/3');
throw new Error('Exception Expected');
} catch (err) {
expect(err.message).toEqual(
`CircleCI artifact URL request failed ` +
`(Missing artifact (some/path/3) for CircleCI build: ${buildNum})`);
}
});
});
});

View File

@ -1,7 +1,5 @@
// Imports // Imports
import {EventEmitter} from 'events'; import * as nock from 'nock';
import {ClientRequest, IncomingMessage} from 'http';
import * as https from 'https';
import {GithubApi} from '../../lib/common/github-api'; import {GithubApi} from '../../lib/common/github-api';
// Tests // Tests
@ -110,39 +108,6 @@ describe('GithubApi', () => {
}); });
// 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('getPaginated()', () => { describe('getPaginated()', () => {
let deferreds: {resolve: (v: any) => void, reject: (v: any) => void}[]; let deferreds: {resolve: (v: any) => void, reject: (v: any) => void}[];
@ -218,191 +183,162 @@ describe('GithubApi', () => {
}); });
// 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()', () => { describe('request()', () => {
let httpsRequestSpy: jasmine.Spy;
let latestRequest: ClientRequest;
beforeEach(() => {
const originalRequest = https.request;
httpsRequestSpy = spyOn(https, 'request').and.callFake((...args: any[]) => {
latestRequest = originalRequest.apply(https, args);
spyOn(latestRequest, 'on').and.callThrough();
spyOn(latestRequest, 'end');
return latestRequest;
});
});
it('should return a promise', () => { it('should return a promise', () => {
nock('https://api.github.com').get('').reply(200);
expect((api as any).request()).toEqual(jasmine.any(Promise)); expect((api as any).request()).toEqual(jasmine.any(Promise));
}); });
it('should call \'https.request()\' with the correct options', () => { it('should call \'https.request()\' with the correct options', () => {
(api as any).request('method', 'path'); const requestHandler = nock('https://api.github.com')
.intercept('/path', 'method')
.reply(200);
expect(httpsRequestSpy).toHaveBeenCalled(); (api as any).request('method', '/path');
expect(httpsRequestSpy.calls.argsFor(0)[0]).toEqual(jasmine.objectContaining({ requestHandler.done();
headers: jasmine.objectContaining({
'User-Agent': `Node/${process.versions.node}`,
}),
host: 'api.github.com',
method: 'method',
path: 'path',
}));
}); });
it('should call specify an \'Authorization\' header if \'githubToken\' is present', () => { it('should add the \'Authorization\' header containing the \'githubToken\'', () => {
(api as any).request('method', 'path'); const requestHandler = nock('https://api.github.com')
.intercept('/path', 'method', undefined, {
expect(httpsRequestSpy).toHaveBeenCalled(); reqheaders: {Authorization: 'token 12345'},
expect(httpsRequestSpy.calls.argsFor(0)[0].headers).toEqual(jasmine.objectContaining({ })
Authorization: 'token 12345', .reply(200);
})); (api as any).request('method', '/path');
requestHandler.done();
}); });
it('should reject on request error', done => { it('should reject on request error', async () => {
(api as any).request('method', 'path').catch((err: any) => { nock('https://api.github.com')
expect(err).toBe('Test'); .intercept('/path', 'method')
done(); .replyWithError('Test');
}); let message = 'Failed to reject error';
await (api as any).request('method', '/path').catch((err: any) => message = err.message);
latestRequest.emit('error', 'Test'); expect(message).toEqual('Test');
});
it('should send the request (i.e. call \'end()\')', () => {
(api as any).request('method', 'path');
expect(latestRequest.end).toHaveBeenCalled();
}); });
it('should \'JSON.stringify\' and send the data along with the request', () => { it('should \'JSON.stringify\' and send the data along with the request', () => {
(api as any).request('method', 'path'); const data = {key: 'value'};
expect(latestRequest.end).toHaveBeenCalledWith(null); const requestHandler = nock('https://api.github.com')
.intercept('/path', 'method', JSON.stringify(data))
(api as any).request('method', 'path', {key: 'value'}); .reply(200);
expect(latestRequest.end).toHaveBeenCalledWith('{"key":"value"}'); (api as any).request('method', '/path', data);
requestHandler.done();
}); });
describe('onResponse', () => { it('should reject if response statusCode is <200', done => {
let promise: Promise<object>; const requestHandler = nock('https://api.github.com')
let respond: (statusCode: number) => IncomingMessage; .intercept('/path', 'method')
.reply(199);
beforeEach(() => { (api as any).request('method', '/path')
promise = (api as any).request('method', 'path'); .catch((err: string) => {
respond = (statusCode: number) => {
const mockResponse = new EventEmitter() as IncomingMessage;
mockResponse.statusCode = statusCode;
const onResponse = httpsRequestSpy.calls.argsFor(0)[1];
onResponse(mockResponse);
return mockResponse;
};
});
it('should reject on response error', done => {
promise.catch(err => {
expect(err).toBe('Test');
done();
});
const res = respond(200);
res.emit('error', 'Test');
});
it('should reject if returned statusCode is <200', done => {
promise.catch(err => {
expect(err).toContain('failed'); expect(err).toContain('failed');
expect(err).toContain('status: 199'); expect(err).toContain('status: 199');
done(); done();
}); });
requestHandler.done();
const res = respond(199);
res.emit('end');
}); });
it('should reject if returned statusCode is >=400', done => { it('should reject if response statusCode is >=400', done => {
promise.catch(err => { const requestHandler = nock('https://api.github.com')
.intercept('/path', 'method')
.reply(400);
(api as any).request('method', '/path')
.catch((err: string) => {
expect(err).toContain('failed'); expect(err).toContain('failed');
expect(err).toContain('status: 400'); expect(err).toContain('status: 400');
done(); done();
}); });
requestHandler.done();
const res = respond(400);
res.emit('end');
}); });
it('should include the response text in the rejection message', done => { it('should include the response text in the rejection message', done => {
promise.catch(err => { const requestHandler = nock('https://api.github.com')
.intercept('/path', 'method')
.reply(500, 'Test');
(api as any).request('method', '/path')
.catch((err: string) => {
expect(err).toContain('Test'); expect(err).toContain('Test');
done(); done();
}); });
requestHandler.done();
const res = respond(500);
res.emit('data', 'Test');
res.emit('end');
}); });
it('should resolve if returned statusCode is <=200 <400', done => { it('should resolve if returned statusCode is >=200 and <400', done => {
promise.then(done); const requestHandler = nock('https://api.github.com')
.intercept('/path', 'method')
.reply(200);
const res = respond(200); (api as any).request('method', '/path').then(done);
res.emit('data', '{}'); requestHandler.done();
res.emit('end');
}); });
it('should resolve with the response text \'JSON.parsed\'', done => { it('should parse the response body into an object using \'JSON.parse\'', done => {
promise.then(data => { const requestHandler = nock('https://api.github.com')
.intercept('/path', 'method')
.reply(300, '{"foo": "bar"}');
(api as any).request('method', '/path').then((data: any) => {
expect(data).toEqual({foo: 'bar'}); expect(data).toEqual({foo: 'bar'});
done(); done();
}); });
requestHandler.done();
const res = respond(300);
res.emit('data', '{"foo":"bar"}');
res.emit('end');
}); });
it('should collect and concatenate the whole response text', done => {
promise.then(data => {
expect(data).toEqual({foo: 'bar', baz: 'qux'});
done();
});
const res = respond(300);
res.emit('data', '{"foo":');
res.emit('data', '"bar","baz"');
res.emit('data', ':"qux"}');
res.emit('end');
});
it('should reject if the response text is malformed JSON', done => { it('should reject if the response text is malformed JSON', done => {
promise.catch(err => { const requestHandler = nock('https://api.github.com')
.intercept('/path', 'method')
.reply(300, '}');
(api as any).request('method', '/path').catch((err: any) => {
expect(err).toEqual(jasmine.any(SyntaxError)); expect(err).toEqual(jasmine.any(SyntaxError));
done(); done();
}); });
requestHandler.done();
const res = respond(300);
res.emit('data', '}');
res.emit('end');
});
}); });
}); });

View File

@ -1,20 +1,27 @@
// Imports // Imports
import {GithubApi} from '../../lib/common/github-api';
import {GithubPullRequests} from '../../lib/common/github-pull-requests'; import {GithubPullRequests} from '../../lib/common/github-pull-requests';
// Tests // Tests
describe('GithubPullRequests', () => { describe('GithubPullRequests', () => {
let githubApi: jasmine.SpyObj<GithubApi>;
beforeEach(() => {
githubApi = jasmine.createSpyObj('githubApi', ['post', 'get', 'getPaginated']);
});
describe('constructor()', () => { describe('constructor()', () => {
it('should throw if \'githubToken\' is missing or empty', () => { it('should throw if \'githubOrg\' is missing or empty', () => {
expect(() => new GithubPullRequests('', 'foo/bar')). expect(() => new GithubPullRequests(githubApi, '', 'bar')).
toThrowError('Missing or empty required parameter \'githubToken\'!'); toThrowError('Missing or empty required parameter \'githubOrg\'!');
}); });
it('should throw if \'repoSlug\' is missing or empty', () => { it('should throw if \'githubRepo\' is missing or empty', () => {
expect(() => new GithubPullRequests('12345', '')). expect(() => new GithubPullRequests(githubApi, 'foo', '')).
toThrowError('Missing or empty required parameter \'repoSlug\'!'); toThrowError('Missing or empty required parameter \'githubRepo\'!');
}); });
}); });
@ -22,17 +29,9 @@ describe('GithubPullRequests', () => {
describe('addComment()', () => { describe('addComment()', () => {
let prs: GithubPullRequests; let prs: GithubPullRequests;
let deferred: {resolve: (v: any) => void, reject: (v: any) => void};
beforeEach(() => { beforeEach(() => {
prs = new GithubPullRequests('12345', 'foo/bar'); prs = new GithubPullRequests(githubApi, 'foo', 'bar');
spyOn(prs, 'post').and.callFake(() => new Promise((resolve, reject) => deferred = {resolve, reject}));
});
it('should return a promise', () => {
expect(prs.addComment(42, 'body')).toEqual(jasmine.any(Promise));
}); });
@ -47,30 +46,28 @@ describe('GithubPullRequests', () => {
}); });
it('should call \'post()\' with the correct pathname, params and data', () => { it('should make a POST request to Github with the correct pathname, params and data', () => {
githubApi.post.and.callFake(() => Promise.resolve());
prs.addComment(42, 'body'); prs.addComment(42, 'body');
expect(githubApi.post).toHaveBeenCalledWith('/repos/foo/bar/issues/42/comments', null, {body: 'body'});
expect(prs.post).toHaveBeenCalledWith('/repos/foo/bar/issues/42/comments', null, {body: 'body'});
}); });
it('should reject if the request fails', done => { it('should reject if the request fails', done => {
githubApi.post.and.callFake(() => Promise.reject('Test'));
prs.addComment(42, 'body').catch(err => { prs.addComment(42, 'body').catch(err => {
expect(err).toBe('Test'); expect(err).toBe('Test');
done(); done();
}); });
deferred.reject('Test');
}); });
it('should resolve with the returned response', done => { it('should resolve with the data from the Github POST', done => {
githubApi.post.and.callFake(() => Promise.resolve('Test'));
prs.addComment(42, 'body').then(data => { prs.addComment(42, 'body').then(data => {
expect(data as any).toBe('Test'); expect(data).toBe('Test');
done(); done();
}); });
deferred.resolve('Test');
}); });
}); });
@ -78,35 +75,34 @@ describe('GithubPullRequests', () => {
describe('fetch()', () => { describe('fetch()', () => {
let prs: GithubPullRequests; let prs: GithubPullRequests;
let prsGetSpy: jasmine.Spy;
beforeEach(() => { beforeEach(() => {
prs = new GithubPullRequests('12345', 'foo/bar'); prs = new GithubPullRequests(githubApi, 'foo', 'bar');
prsGetSpy = spyOn(prs as any, 'get');
}); });
it('should call \'get()\' with the correct pathname', () => { it('should make a GET request to GitHub with the correct pathname', () => {
prs.fetch(42); prs.fetch(42);
expect(prsGetSpy).toHaveBeenCalledWith('/repos/foo/bar/issues/42'); expect(githubApi.get).toHaveBeenCalledWith('/repos/foo/bar/issues/42');
}); });
it('should forward the value returned by \'get()\'', () => { it('should resolve with the data returned from GitHub', done => {
prsGetSpy.and.returnValue('Test'); const expected: any = {number: 42};
expect(prs.fetch(42) as any).toBe('Test'); githubApi.get.and.callFake(() => Promise.resolve(expected));
prs.fetch(42).then(data => {
expect(data).toEqual(expected);
done();
});
}); });
}); });
describe('fetchAll()', () => { describe('fetchAll()', () => {
let prs: GithubPullRequests; let prs: GithubPullRequests;
let prsGetPaginatedSpy: jasmine.Spy;
beforeEach(() => { beforeEach(() => {
prs = new GithubPullRequests('12345', 'foo/bar'); prs = new GithubPullRequests(githubApi, 'foo', 'bar');
prsGetPaginatedSpy = spyOn(prs as any, 'getPaginated');
spyOn(console, 'log'); spyOn(console, 'log');
}); });
@ -118,24 +114,48 @@ describe('GithubPullRequests', () => {
prs.fetchAll('closed'); prs.fetchAll('closed');
prs.fetchAll('open'); prs.fetchAll('open');
expect(prsGetPaginatedSpy).toHaveBeenCalledTimes(3); expect(githubApi.getPaginated).toHaveBeenCalledTimes(3);
expect(prsGetPaginatedSpy.calls.argsFor(0)).toEqual([expectedPathname, {state: 'all'}]); expect(githubApi.getPaginated.calls.argsFor(0)).toEqual([expectedPathname, {state: 'all'}]);
expect(prsGetPaginatedSpy.calls.argsFor(1)).toEqual([expectedPathname, {state: 'closed'}]); expect(githubApi.getPaginated.calls.argsFor(1)).toEqual([expectedPathname, {state: 'closed'}]);
expect(prsGetPaginatedSpy.calls.argsFor(2)).toEqual([expectedPathname, {state: 'open'}]); expect(githubApi.getPaginated.calls.argsFor(2)).toEqual([expectedPathname, {state: 'open'}]);
}); });
it('should default to \'all\' if no state is specified', () => { it('should default to \'all\' if no state is specified', () => {
prs.fetchAll(); prs.fetchAll();
expect(prsGetPaginatedSpy).toHaveBeenCalledWith('/repos/foo/bar/pulls', {state: 'all'}); expect(githubApi.getPaginated).toHaveBeenCalledWith('/repos/foo/bar/pulls', {state: 'all'});
}); });
it('should forward the value returned by \'getPaginated()\'', () => { it('should forward the value returned by \'getPaginated()\'', () => {
prsGetPaginatedSpy.and.returnValue('Test'); githubApi.getPaginated.and.returnValue('Test');
expect(prs.fetchAll() as any).toBe('Test'); expect(prs.fetchAll() as any).toBe('Test');
}); });
}); });
describe('fetchFiles()', () => {
let prs: GithubPullRequests;
beforeEach(() => {
prs = new GithubPullRequests(githubApi, 'foo', 'bar');
});
it('should make a GET request to GitHub with the correct pathname', () => {
prs.fetchFiles(42);
expect(githubApi.get).toHaveBeenCalledWith('/repos/foo/bar/pulls/42/files');
});
it('should resolve with the data returned from GitHub', done => {
const expected: any = [{ sha: 'ABCDE', filename: 'a/b/c'}, { sha: '12345', filename: 'x/y/z' }];
githubApi.get.and.callFake(() => Promise.resolve(expected));
prs.fetch(42).then(data => {
expect(data).toEqual(expected);
done();
});
});
});
}); });

View File

@ -1,43 +1,40 @@
// Imports import {GithubApi} from '../../lib/common/github-api';
import {GithubTeams} from '../../lib/common/github-teams'; import {GithubTeams} from '../../lib/common/github-teams';
// Tests // Tests
describe('GithubTeams', () => { describe('GithubTeams', () => {
let githubApi: jasmine.SpyObj<GithubApi>;
beforeEach(() => {
githubApi = jasmine.createSpyObj('githubApi', ['post', 'get', 'getPaginated']);
});
describe('constructor()', () => { describe('constructor()', () => {
it('should throw if \'githubToken\' is missing or empty', () => { it('should throw if \'githubOrg\' is missing or empty', () => {
expect(() => new GithubTeams('', 'org')). expect(() => new GithubTeams(githubApi, '')).
toThrowError('Missing or empty required parameter \'githubToken\'!'); toThrowError('Missing or empty required parameter \'githubOrg\'!');
}); });
it('should throw if \'organization\' is missing or empty', () => {
expect(() => new GithubTeams('12345', '')).
toThrowError('Missing or empty required parameter \'organization\'!');
});
}); });
describe('fetchAll()', () => { describe('fetchAll()', () => {
let teams: GithubTeams; let teams: GithubTeams;
let teamsGetPaginatedSpy: jasmine.Spy;
beforeEach(() => { beforeEach(() => {
teams = new GithubTeams('12345', 'foo'); teams = new GithubTeams(githubApi, 'foo');
teamsGetPaginatedSpy = spyOn(teams as any, 'getPaginated');
}); });
it('should call \'getPaginated()\' with the correct pathname and params', () => { it('should call \'getPaginated()\' with the correct pathname and params', () => {
teams.fetchAll(); teams.fetchAll();
expect(teamsGetPaginatedSpy).toHaveBeenCalledWith('/orgs/foo/teams'); expect(githubApi.getPaginated).toHaveBeenCalledWith('/orgs/foo/teams');
}); });
it('should forward the value returned by \'getPaginated()\'', () => { it('should forward the value returned by \'getPaginated()\'', () => {
teamsGetPaginatedSpy.and.returnValue('Test'); githubApi.getPaginated.and.returnValue('Test');
expect(teams.fetchAll() as any).toBe('Test'); expect(teams.fetchAll() as any).toBe('Test');
}); });
@ -46,19 +43,15 @@ describe('GithubTeams', () => {
describe('isMemberById()', () => { describe('isMemberById()', () => {
let teams: GithubTeams; let teams: GithubTeams;
let teamsGetSpy: jasmine.Spy;
beforeEach(() => { beforeEach(() => {
teams = new GithubTeams('12345', 'foo'); teams = new GithubTeams(githubApi, 'foo');
teamsGetSpy = spyOn(teams, 'get').and.returnValue(Promise.resolve(null));
}); });
it('should return a promise', done => { it('should return a promise', () => {
githubApi.get.and.callFake(() => Promise.resolve());
const promise = teams.isMemberById('user', [1]); const promise = teams.isMemberById('user', [1]);
promise.then(done); // Do not complete the test (and release the spies) synchronously
// to avoid running the actual `get()`.
expect(promise).toEqual(jasmine.any(Promise)); expect(promise).toEqual(jasmine.any(Promise));
}); });
@ -66,42 +59,43 @@ describe('GithubTeams', () => {
it('should resolve with false if called with an empty array', done => { it('should resolve with false if called with an empty array', done => {
teams.isMemberById('user', []).then(isMember => { teams.isMemberById('user', []).then(isMember => {
expect(isMember).toBe(false); expect(isMember).toBe(false);
expect(teamsGetSpy).not.toHaveBeenCalled(); expect(githubApi.get).not.toHaveBeenCalled();
done(); done();
}); });
}); });
it('should call \'get()\' with the correct pathname', done => { it('should call \'get()\' with the correct pathname', done => {
githubApi.get.and.callFake(() => Promise.resolve());
teams.isMemberById('user', [1]).then(() => { teams.isMemberById('user', [1]).then(() => {
expect(teamsGetSpy).toHaveBeenCalledWith('/teams/1/memberships/user'); expect(githubApi.get).toHaveBeenCalledWith('/teams/1/memberships/user');
done(); done();
}); });
}); });
it('should resolve with false if \'get()\' rejects', done => { it('should resolve with false if \'get()\' rejects', done => {
teamsGetSpy.and.returnValue(Promise.reject(null)); githubApi.get.and.callFake(() => Promise.reject(null));
teams.isMemberById('user', [1]).then(isMember => { teams.isMemberById('user', [1]).then(isMember => {
expect(isMember).toBe(false); expect(isMember).toBe(false);
expect(teamsGetSpy).toHaveBeenCalled(); expect(githubApi.get).toHaveBeenCalled();
done(); done();
}); });
}); });
it('should resolve with false if the membership is not active', done => { it('should resolve with false if the membership is not active', done => {
teamsGetSpy.and.returnValue(Promise.resolve({state: 'pending'})); githubApi.get.and.callFake(() => Promise.resolve({state: 'pending'}));
teams.isMemberById('user', [1]).then(isMember => { teams.isMemberById('user', [1]).then(isMember => {
expect(isMember).toBe(false); expect(isMember).toBe(false);
expect(teamsGetSpy).toHaveBeenCalled(); expect(githubApi.get).toHaveBeenCalled();
done(); done();
}); });
}); });
it('should resolve with true if the membership is active', done => { it('should resolve with true if the membership is active', done => {
teamsGetSpy.and.returnValue(Promise.resolve({state: 'active'})); githubApi.get.and.callFake(() => Promise.resolve({state: 'active'}));
teams.isMemberById('user', [1]).then(isMember => { teams.isMemberById('user', [1]).then(isMember => {
expect(isMember).toBe(true); expect(isMember).toBe(true);
done(); done();
@ -115,15 +109,15 @@ describe('GithubTeams', () => {
'/teams/2/memberships/user': Promise.reject(null), '/teams/2/memberships/user': Promise.reject(null),
'/teams/3/memberships/user': Promise.resolve({state: 'active'}), '/teams/3/memberships/user': Promise.resolve({state: 'active'}),
}; };
teamsGetSpy.and.callFake((pathname: string) => trainedResponses[pathname]); githubApi.get.and.callFake((pathname: string) => trainedResponses[pathname]);
teams.isMemberById('user', [1, 2, 3, 4]).then(isMember => { teams.isMemberById('user', [1, 2, 3, 4]).then(isMember => {
expect(isMember).toBe(true); expect(isMember).toBe(true);
expect(teamsGetSpy).toHaveBeenCalledTimes(3); expect(githubApi.get).toHaveBeenCalledTimes(3);
expect(teamsGetSpy.calls.argsFor(0)[0]).toBe('/teams/1/memberships/user'); expect(githubApi.get.calls.argsFor(0)[0]).toBe('/teams/1/memberships/user');
expect(teamsGetSpy.calls.argsFor(1)[0]).toBe('/teams/2/memberships/user'); expect(githubApi.get.calls.argsFor(1)[0]).toBe('/teams/2/memberships/user');
expect(teamsGetSpy.calls.argsFor(2)[0]).toBe('/teams/3/memberships/user'); expect(githubApi.get.calls.argsFor(2)[0]).toBe('/teams/3/memberships/user');
done(); done();
}); });
@ -137,16 +131,16 @@ describe('GithubTeams', () => {
'/teams/3/memberships/user': Promise.resolve({state: 'not active'}), '/teams/3/memberships/user': Promise.resolve({state: 'not active'}),
'/teams/4/memberships/user': Promise.reject(null), '/teams/4/memberships/user': Promise.reject(null),
}; };
teamsGetSpy.and.callFake((pathname: string) => trainedResponses[pathname]); githubApi.get.and.callFake((pathname: string) => trainedResponses[pathname]);
teams.isMemberById('user', [1, 2, 3, 4]).then(isMember => { teams.isMemberById('user', [1, 2, 3, 4]).then(isMember => {
expect(isMember).toBe(false); expect(isMember).toBe(false);
expect(teamsGetSpy).toHaveBeenCalledTimes(4); expect(githubApi.get).toHaveBeenCalledTimes(4);
expect(teamsGetSpy.calls.argsFor(0)[0]).toBe('/teams/1/memberships/user'); expect(githubApi.get.calls.argsFor(0)[0]).toBe('/teams/1/memberships/user');
expect(teamsGetSpy.calls.argsFor(1)[0]).toBe('/teams/2/memberships/user'); expect(githubApi.get.calls.argsFor(1)[0]).toBe('/teams/2/memberships/user');
expect(teamsGetSpy.calls.argsFor(2)[0]).toBe('/teams/3/memberships/user'); expect(githubApi.get.calls.argsFor(2)[0]).toBe('/teams/3/memberships/user');
expect(teamsGetSpy.calls.argsFor(3)[0]).toBe('/teams/4/memberships/user'); expect(githubApi.get.calls.argsFor(3)[0]).toBe('/teams/4/memberships/user');
done(); done();
}); });
@ -161,7 +155,7 @@ describe('GithubTeams', () => {
let teamsIsMemberByIdSpy: jasmine.Spy; let teamsIsMemberByIdSpy: jasmine.Spy;
beforeEach(() => { beforeEach(() => {
teams = new GithubTeams('12345', 'foo'); teams = new GithubTeams(githubApi, 'foo');
const mockResponse = Promise.resolve([{id: 1, slug: 'team1'}, {id: 2, slug: 'team2'}]); const mockResponse = Promise.resolve([{id: 1, slug: 'team1'}, {id: 2, slug: 'team2'}]);
teamsFetchAllSpy = spyOn(teams, 'fetchAll').and.returnValue(mockResponse); teamsFetchAllSpy = spyOn(teams, 'fetchAll').and.returnValue(mockResponse);
@ -181,7 +175,7 @@ describe('GithubTeams', () => {
it('should resolve with false if \'fetchAll()\' rejects', done => { it('should resolve with false if \'fetchAll()\' rejects', done => {
teamsFetchAllSpy.and.returnValue(Promise.reject(null)); teamsFetchAllSpy.and.callFake(() => Promise.reject(null));
teams.isMemberBySlug('user', ['team-slug']).then(isMember => { teams.isMemberBySlug('user', ['team-slug']).then(isMember => {
expect(isMember).toBe(false); expect(isMember).toBe(false);
done(); done();
@ -209,7 +203,7 @@ describe('GithubTeams', () => {
it('should resolve with false if \'isMemberById()\' rejects', done => { it('should resolve with false if \'isMemberById()\' rejects', done => {
teamsIsMemberByIdSpy.and.returnValue(Promise.reject(null)); teamsIsMemberByIdSpy.and.callFake(() => Promise.reject(null));
teams.isMemberBySlug('user', ['team1']).then(isMember => { teams.isMemberBySlug('user', ['team1']).then(isMember => {
expect(isMember).toBe(false); expect(isMember).toBe(false);
expect(teamsIsMemberByIdSpy).toHaveBeenCalled(); expect(teamsIsMemberByIdSpy).toHaveBeenCalled();
@ -218,16 +212,17 @@ describe('GithubTeams', () => {
}); });
it('should resolve with the value \'isMemberById()\' resolves with', done => { it('should resolve with the value \'isMemberById()\' resolves with', async () => {
teamsIsMemberByIdSpy.and.returnValues(Promise.resolve(false), Promise.resolve(true));
Promise.all([ teamsIsMemberByIdSpy.and.callFake(() => Promise.resolve(true));
teams.isMemberBySlug('user', ['team1']).then(isMember => expect(isMember).toBe(false)), const isMember1 = await teams.isMemberBySlug('user', ['team1']);
teams.isMemberBySlug('user', ['team1']).then(isMember => expect(isMember).toBe(true)), expect(isMember1).toBe(true);
]).then(() => { expect(teamsIsMemberByIdSpy).toHaveBeenCalledWith('user', [1]);
expect(teamsIsMemberByIdSpy).toHaveBeenCalledTimes(2);
done(); teamsIsMemberByIdSpy.and.callFake(() => Promise.resolve(false));
}); const isMember2 = await teams.isMemberBySlug('user', ['team1']);
expect(isMember2).toBe(false);
expect(teamsIsMemberByIdSpy).toHaveBeenCalledWith('user', [1]);
}); });
}); });

View File

@ -1,9 +1,53 @@
// Imports // Imports
import {assertNotMissingOrEmpty, getEnvVar} from '../../lib/common/utils'; import {
assert,
assertNotMissingOrEmpty,
computeArtifactDownloadPath,
computeShortSha,
getEnvVar,
getPrInfoFromDownloadPath,
} from '../../lib/common/utils';
// Tests // Tests
describe('utils', () => { 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).toEqual('/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()', () => { describe('assertNotMissingOrEmpty()', () => {
it('should throw if passed an empty value', () => { it('should throw if passed an empty value', () => {

View File

@ -5,20 +5,20 @@ import * as fs from 'fs';
import * as path from 'path'; import * as path from 'path';
import * as shell from 'shelljs'; import * as shell from 'shelljs';
import {SHORT_SHA_LEN} from '../../lib/common/constants'; import {SHORT_SHA_LEN} from '../../lib/common/constants';
import {BuildCreator} from '../../lib/upload-server/build-creator'; import {BuildCreator} from '../../lib/preview-server/build-creator';
import {ChangedPrVisibilityEvent, CreatedBuildEvent} from '../../lib/upload-server/build-events'; import {ChangedPrVisibilityEvent, CreatedBuildEvent} from '../../lib/preview-server/build-events';
import {UploadError} from '../../lib/upload-server/upload-error'; import {PreviewServerError} from '../../lib/preview-server/preview-error';
import {expectToBeUploadError} from './helpers'; import {expectToBePreviewServerError} from './helpers';
// Tests // Tests
describe('BuildCreator', () => { describe('BuildCreator', () => {
const pr = '9'; const pr = 9;
const sha = '9'.repeat(40); const sha = '9'.repeat(40);
const shortSha = sha.substr(0, SHORT_SHA_LEN); const shortSha = sha.substr(0, SHORT_SHA_LEN);
const archive = 'snapshot.tar.gz'; const archive = 'snapshot.tar.gz';
const buildsDir = 'builds/dir'; const buildsDir = 'builds/dir';
const hiddenPrDir = path.join(buildsDir, `hidden--${pr}`); const hiddenPrDir = path.join(buildsDir, `hidden--${pr}`);
const publicPrDir = path.join(buildsDir, pr); const publicPrDir = path.join(buildsDir, `${pr}`);
const hiddenShaDir = path.join(hiddenPrDir, shortSha); const hiddenShaDir = path.join(hiddenPrDir, shortSha);
const publicShaDir = path.join(publicPrDir, shortSha); const publicShaDir = path.join(publicPrDir, shortSha);
let bc: BuildCreator; let bc: BuildCreator;
@ -134,8 +134,8 @@ describe('BuildCreator', () => {
it('should abort and skip further operations if changing the PR\'s visibility fails', done => { it('should abort and skip further operations if changing the PR\'s visibility fails', done => {
const mockError = new UploadError(543, 'Test'); const mockError = new PreviewServerError(543, 'Test');
bcUpdatePrVisibilitySpy.and.returnValue(Promise.reject(mockError)); bcUpdatePrVisibilitySpy.and.callFake(() => Promise.reject(mockError));
bc.create(pr, sha, archive, isPublic).catch(err => { bc.create(pr, sha, archive, isPublic).catch(err => {
expect(err).toBe(mockError); expect(err).toBe(mockError);
@ -154,7 +154,7 @@ describe('BuildCreator', () => {
existsValues[shaDir] = true; existsValues[shaDir] = true;
bc.create(pr, sha, archive, isPublic).catch(err => { bc.create(pr, sha, archive, isPublic).catch(err => {
const publicOrNot = isPublic ? 'public' : 'non-public'; const publicOrNot = isPublic ? 'public' : 'non-public';
expectToBeUploadError(err, 409, `Request to overwrite existing ${publicOrNot} directory: ${shaDir}`); expectToBePreviewServerError(err, 409, `Request to overwrite existing ${publicOrNot} directory: ${shaDir}`);
expect(shellMkdirSpy).not.toHaveBeenCalled(); expect(shellMkdirSpy).not.toHaveBeenCalled();
expect(bcExtractArchiveSpy).not.toHaveBeenCalled(); expect(bcExtractArchiveSpy).not.toHaveBeenCalled();
expect(bcEmitSpy).not.toHaveBeenCalled(); expect(bcEmitSpy).not.toHaveBeenCalled();
@ -171,7 +171,7 @@ describe('BuildCreator', () => {
bc.create(pr, sha, archive, isPublic).catch(err => { bc.create(pr, sha, archive, isPublic).catch(err => {
const publicOrNot = isPublic ? 'public' : 'non-public'; const publicOrNot = isPublic ? 'public' : 'non-public';
expectToBeUploadError(err, 409, `Request to overwrite existing ${publicOrNot} directory: ${shaDir}`); expectToBePreviewServerError(err, 409, `Request to overwrite existing ${publicOrNot} directory: ${shaDir}`);
expect(shellMkdirSpy).not.toHaveBeenCalled(); expect(shellMkdirSpy).not.toHaveBeenCalled();
expect(bcExtractArchiveSpy).not.toHaveBeenCalled(); expect(bcExtractArchiveSpy).not.toHaveBeenCalled();
expect(bcEmitSpy).not.toHaveBeenCalled(); expect(bcEmitSpy).not.toHaveBeenCalled();
@ -222,20 +222,20 @@ describe('BuildCreator', () => {
}); });
it('should reject with an UploadError', done => { it('should reject with an PreviewServerError', done => {
// tslint:disable-next-line: no-string-throw // tslint:disable-next-line: no-string-throw
shellMkdirSpy.and.callFake(() => { throw 'Test'; }); shellMkdirSpy.and.callFake(() => { throw 'Test'; });
bc.create(pr, sha, archive, isPublic).catch(err => { bc.create(pr, sha, archive, isPublic).catch(err => {
expectToBeUploadError(err, 500, `Error while uploading to directory: ${shaDir}\nTest`); expectToBePreviewServerError(err, 500, `Error while creating preview at: ${shaDir}\nTest`);
done(); done();
}); });
}); });
it('should pass UploadError instances unmodified', done => { it('should pass PreviewServerError instances unmodified', done => {
shellMkdirSpy.and.callFake(() => { throw new UploadError(543, 'Test'); }); shellMkdirSpy.and.callFake(() => { throw new PreviewServerError(543, 'Test'); });
bc.create(pr, sha, archive, isPublic).catch(err => { bc.create(pr, sha, archive, isPublic).catch(err => {
expectToBeUploadError(err, 543, 'Test'); expectToBePreviewServerError(err, 543, 'Test');
done(); done();
}); });
}); });
@ -324,7 +324,7 @@ describe('BuildCreator', () => {
const shas = ['foo', 'bar', 'baz']; const shas = ['foo', 'bar', 'baz'];
let emitted = false; let emitted = false;
bcListShasByDate.and.returnValue(Promise.resolve(shas)); bcListShasByDate.and.callFake(() => Promise.resolve(shas));
bcEmitSpy.and.callFake((type: string, evt: ChangedPrVisibilityEvent) => { bcEmitSpy.and.callFake((type: string, evt: ChangedPrVisibilityEvent) => {
expect(bcListShasByDate).toHaveBeenCalledWith(newPrDir); expect(bcListShasByDate).toHaveBeenCalledWith(newPrDir);
@ -376,7 +376,8 @@ describe('BuildCreator', () => {
it('should abort and skip further operations if both directories exist', done => { it('should abort and skip further operations if both directories exist', done => {
bcExistsSpy.and.returnValue(true); bcExistsSpy.and.returnValue(true);
bc.updatePrVisibility(pr, makePublic).catch(err => { bc.updatePrVisibility(pr, makePublic).catch(err => {
expectToBeUploadError(err, 409, `Request to move '${oldPrDir}' to existing directory '${newPrDir}'.`); expectToBePreviewServerError(err, 409,
`Request to move '${oldPrDir}' to existing directory '${newPrDir}'.`);
expect(shellMvSpy).not.toHaveBeenCalled(); expect(shellMvSpy).not.toHaveBeenCalled();
expect(bcListShasByDate).not.toHaveBeenCalled(); expect(bcListShasByDate).not.toHaveBeenCalled();
expect(bcEmitSpy).not.toHaveBeenCalled(); expect(bcEmitSpy).not.toHaveBeenCalled();
@ -407,20 +408,21 @@ describe('BuildCreator', () => {
}); });
it('should reject with an UploadError', done => { it('should reject with an PreviewServerError', done => {
// tslint:disable-next-line: no-string-throw // tslint:disable-next-line: no-string-throw
shellMvSpy.and.callFake(() => { throw 'Test'; }); shellMvSpy.and.callFake(() => { throw 'Test'; });
bc.updatePrVisibility(pr, makePublic).catch(err => { bc.updatePrVisibility(pr, makePublic).catch(err => {
expectToBeUploadError(err, 500, `Error while making PR ${pr} ${makePublic ? 'public' : 'hidden'}.\nTest`); expectToBePreviewServerError(err, 500,
`Error while making PR ${pr} ${makePublic ? 'public' : 'hidden'}.\nTest`);
done(); done();
}); });
}); });
it('should pass UploadError instances unmodified', done => { it('should pass PreviewServerError instances unmodified', done => {
shellMvSpy.and.callFake(() => { throw new UploadError(543, 'Test'); }); shellMvSpy.and.callFake(() => { throw new PreviewServerError(543, 'Test'); });
bc.updatePrVisibility(pr, makePublic).catch(err => { bc.updatePrVisibility(pr, makePublic).catch(err => {
expectToBeUploadError(err, 543, 'Test'); expectToBePreviewServerError(err, 543, 'Test');
done(); done();
}); });
}); });
@ -451,7 +453,7 @@ describe('BuildCreator', () => {
it('should call \'fs.access()\' with the specified argument', () => { it('should call \'fs.access()\' with the specified argument', () => {
(bc as any).exists('foo'); (bc as any).exists('foo');
expect(fs.access).toHaveBeenCalledWith('foo', jasmine.any(Function)); expect(fsAccessSpy).toHaveBeenCalledWith('foo', jasmine.any(Function));
}); });
@ -511,7 +513,8 @@ describe('BuildCreator', () => {
it('should log (as a warning) any stderr output if extracting succeeded', done => { it('should log (as a warning) any stderr output if extracting succeeded', done => {
(bc as any).extractArchive('foo', 'bar'). (bc as any).extractArchive('foo', 'bar').
then(() => expect(consoleWarnSpy).toHaveBeenCalledWith('This is stderr')). then(() => expect(consoleWarnSpy)
.toHaveBeenCalledWith(jasmine.any(String), 'BuildCreator: ', 'This is stderr')).
then(done); then(done);
cpExecCbs[0](null, 'This is stdout', 'This is stderr'); cpExecCbs[0](null, 'This is stdout', 'This is stderr');
@ -527,7 +530,7 @@ describe('BuildCreator', () => {
}); });
it('should delete the uploaded file on success', done => { it('should delete the build artifact file on success', done => {
(bc as any).extractArchive('input/file', 'output/dir'). (bc as any).extractArchive('input/file', 'output/dir').
then(() => expect(shellRmSpy).toHaveBeenCalledWith('-f', 'input/file')). then(() => expect(shellRmSpy).toHaveBeenCalledWith('-f', 'input/file')).
then(done); then(done);
@ -567,7 +570,7 @@ describe('BuildCreator', () => {
}); });
it('should abort and reject if it fails to remove the uploaded file', done => { it('should abort and reject if it fails to remove the build artifact file', done => {
(bc as any).extractArchive('foo', 'bar').catch((err: any) => { (bc as any).extractArchive('foo', 'bar').catch((err: any) => {
expect(shellChmodSpy).toHaveBeenCalled(); expect(shellChmodSpy).toHaveBeenCalled();
expect(shellRmSpy).toHaveBeenCalled(); expect(shellRmSpy).toHaveBeenCalled();
@ -618,7 +621,7 @@ describe('BuildCreator', () => {
it('should reject if listing files fails', done => { it('should reject if listing files fails', done => {
shellLsSpy.and.returnValue(Promise.reject('Test')); shellLsSpy.and.callFake(() => Promise.reject('Test'));
(bc as any).listShasByDate('input/dir').catch((err: string) => { (bc as any).listShasByDate('input/dir').catch((err: string) => {
expect(err).toBe('Test'); expect(err).toBe('Test');
done(); done();
@ -627,7 +630,7 @@ describe('BuildCreator', () => {
it('should return the filenames', done => { it('should return the filenames', done => {
shellLsSpy.and.returnValue(Promise.resolve([ shellLsSpy.and.callFake(() => Promise.resolve([
lsResult('foo', 100), lsResult('foo', 100),
lsResult('bar', 200), lsResult('bar', 200),
lsResult('baz', 300), lsResult('baz', 300),
@ -640,7 +643,7 @@ describe('BuildCreator', () => {
it('should sort by date', done => { it('should sort by date', done => {
shellLsSpy.and.returnValue(Promise.resolve([ shellLsSpy.and.callFake(() => Promise.resolve([
lsResult('foo', 300), lsResult('foo', 300),
lsResult('bar', 100), lsResult('bar', 100),
lsResult('baz', 200), lsResult('baz', 200),
@ -660,7 +663,7 @@ describe('BuildCreator', () => {
]; ];
mockArray.sort = jasmine.createSpy('sort'); mockArray.sort = jasmine.createSpy('sort');
shellLsSpy.and.returnValue(Promise.resolve(mockArray)); shellLsSpy.and.callFake(() => Promise.resolve(mockArray));
(bc as any).listShasByDate('input/dir'). (bc as any).listShasByDate('input/dir').
then((shas: string[]) => { then((shas: string[]) => {
expect(shas).toEqual(['bar', 'baz', 'foo']); expect(shas).toEqual(['bar', 'baz', 'foo']);
@ -671,7 +674,7 @@ describe('BuildCreator', () => {
it('should only include directories', done => { it('should only include directories', done => {
shellLsSpy.and.returnValue(Promise.resolve([ shellLsSpy.and.callFake(() => Promise.resolve([
lsResult('foo', 100), lsResult('foo', 100),
lsResult('bar', 200, false), lsResult('bar', 200, false),
lsResult('baz', 300), lsResult('baz', 300),

View File

@ -1,5 +1,5 @@
// Imports // Imports
import {ChangedPrVisibilityEvent, CreatedBuildEvent} from '../../lib/upload-server/build-events'; import {ChangedPrVisibilityEvent, CreatedBuildEvent} from '../../lib/preview-server/build-events';
// Tests // Tests
describe('ChangedPrVisibilityEvent', () => { describe('ChangedPrVisibilityEvent', () => {

View File

@ -0,0 +1,191 @@
import * as fs from 'fs';
import * as nock from 'nock';
import {BuildInfo, CircleCiApi} from '../../lib/common/circle-ci-api';
import {BuildRetriever} from '../../lib/preview-server/build-retriever';
describe('BuildRetriever', () => {
const MAX_DOWNLOAD_SIZE = 10000;
const DOWNLOAD_DIR = '/DOWNLOAD/DIR';
const BASE_URL = 'http://test.com';
const ARTIFACT_PATH = '/some/path/build.zip';
let api: CircleCiApi;
let BUILD_INFO: BuildInfo;
let WRITEFILE_RESULT: any;
let writeFileSpy: jasmine.Spy;
let EXISTS_RESULT: boolean;
let existsSpy: jasmine.Spy;
let getBuildArtifactUrlSpy: jasmine.Spy;
beforeEach(() => {
BUILD_INFO = {
branch: 'pull/777',
build_num: 12345,
failed: false,
has_artifacts: true,
outcome: 'success',
reponame: 'REPO',
username: 'ORG',
vcs_revision: 'COMMIT',
};
spyOn(console, 'log');
spyOn(console, 'warn');
spyOn(console, 'error');
api = new CircleCiApi('ORG', 'REPO', 'TOKEN');
spyOn(api, 'getBuildInfo').and.callFake(() => Promise.resolve(BUILD_INFO));
getBuildArtifactUrlSpy = spyOn(api, 'getBuildArtifactUrl')
.and.callFake(() => Promise.resolve(BASE_URL + ARTIFACT_PATH));
WRITEFILE_RESULT = undefined;
writeFileSpy = spyOn(fs, 'writeFile').and.callFake(
(_path: string, _buffer: Buffer, callback: (err?: any) => {}) => callback(WRITEFILE_RESULT),
);
EXISTS_RESULT = false;
existsSpy = spyOn(fs, 'exists').and.callFake(
(_path: string, callback: (exists: boolean) => {}) => callback(EXISTS_RESULT),
);
});
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 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(info).toEqual({org: 'ORG', pr: 777, repo: 'REPO', sha: 'COMMIT', success: true});
});
it('should error if it is not possible to extract the PR number from the branch', async () => {
const retriever = new BuildRetriever(api, MAX_DOWNLOAD_SIZE, DOWNLOAD_DIR);
try {
BUILD_INFO.branch = 'master';
await retriever.getGithubInfo(12345);
throw new Error('Exception Expected');
} catch (error) {
expect(error.message).toEqual('No PR found in branch field: master');
}
});
});
describe('downloadBuildArtifact', () => {
const ARTIFACT_CONTENTS = 'ARTIFACT CONTENTS';
let retriever: BuildRetriever;
beforeEach(() => {
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);
try {
await retriever.downloadBuildArtifact(12345, 777, 'COMMIT', ARTIFACT_PATH);
throw new Error('Exception Expected');
} catch (error) {
expect(error.status).toEqual(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);
await retriever.downloadBuildArtifact(12345, 777, 'COMMIT', ARTIFACT_PATH);
expect(writeFileSpy)
.toHaveBeenCalledWith(`${DOWNLOAD_DIR}/777-COMMIT-build.zip`, 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 () => {
try {
getBuildArtifactUrlSpy.and.callFake(() => Promise.reject('getBuildArtifactUrl failed'));
await retriever.downloadBuildArtifact(12345, 777, 'COMMIT', ARTIFACT_PATH);
throw new Error('Exception Expected');
} catch (error) {
expect(error.message).toEqual('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');
try {
await retriever.downloadBuildArtifact(12345, 777, 'COMMIT', ARTIFACT_PATH);
throw new Error('Exception Expected');
} catch (error) {
expect(error.message).toEqual('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');
try {
await retriever.downloadBuildArtifact(12345, 777, 'COMMIT', ARTIFACT_PATH);
throw new Error('Exception Expected');
} catch (error) {
expect(error.message).toEqual('CircleCI artifact download failed (Error 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);
try {
WRITEFILE_RESULT = 'Test Error';
await retriever.downloadBuildArtifact(12345, 777, 'COMMIT', ARTIFACT_PATH);
throw new Error('Exception Expected');
} catch (error) {
expect(error.message).toEqual('CircleCI artifact download failed (Test Error)');
}
artifactRequest.done();
});
});
});

View File

@ -0,0 +1,180 @@
// 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.callFake(() => Promise.resolve([{filename: 'a/b/c'}, {filename: 'd/e/f'}]));
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.callFake(() => Promise.resolve(mockPrInfo));
teamsIsMemberBySlugSpy = spyOn(GithubTeams.prototype, 'isMemberBySlug').
and.callFake(() => Promise.resolve(true));
});
it('should return a promise', done => {
const promise = bv.getPrIsTrusted(pr);
promise.then(done); // Do not complete the test (and release the spies) synchronously
// to avoid running the actual `GithubTeams#isMemberBySlug()`.
expect(promise).toEqual(jasmine.any(Promise));
});
it('should fetch the corresponding PR', done => {
bv.getPrIsTrusted(pr).then(() => {
expect(prsFetchSpy).toHaveBeenCalledWith(pr);
done();
});
});
it('should fail if fetching the PR errors', done => {
prsFetchSpy.and.callFake(() => Promise.reject('Test'));
bv.getPrIsTrusted(pr).catch(err => {
expect(err).toBe('Test');
done();
});
});
describe('when the PR has the "trusted PR" label', () => {
beforeEach(() => mockPrInfo.labels.push({name: 'trusted: pr-label'}));
it('should resolve to true', done => {
bv.getPrIsTrusted(pr).then(isTrusted => {
expect(isTrusted).toBe(true);
done();
});
});
it('should not try to verify the author\'s membership status', done => {
bv.getPrIsTrusted(pr).then(() => {
expect(teamsIsMemberBySlugSpy).not.toHaveBeenCalled();
done();
});
});
});
describe('when the PR does not have the "trusted PR" label', () => {
it('should verify the PR author\'s membership in the specified teams', done => {
bv.getPrIsTrusted(pr).then(() => {
expect(teamsIsMemberBySlugSpy).toHaveBeenCalledWith('username', ['team1', 'team2']);
done();
});
});
it('should fail if verifying membership errors', done => {
teamsIsMemberBySlugSpy.and.callFake(() => Promise.reject('Test'));
bv.getPrIsTrusted(pr).catch(err => {
expect(err).toBe('Test');
done();
});
});
it('should resolve to true if the PR\'s author is a member', done => {
teamsIsMemberBySlugSpy.and.callFake(() => Promise.resolve(true));
bv.getPrIsTrusted(pr).then(isTrusted => {
expect(isTrusted).toBe(true);
done();
});
});
it('should resolve to false if the PR\'s author is not a member', done => {
teamsIsMemberBySlugSpy.and.callFake(() => Promise.resolve(false));
bv.getPrIsTrusted(pr).then(isTrusted => {
expect(isTrusted).toBe(false);
done();
});
});
});
});
});

View File

@ -0,0 +1,11 @@
import {PreviewServerError} from '../../lib/preview-server/preview-error';
export const expectToBePreviewServerError = (actual: PreviewServerError, status?: number, message?: string) => {
expect(actual).toEqual(jasmine.any(PreviewServerError));
if (status != null) {
expect(actual.status).toBe(status);
}
if (message != null) {
expect(actual.message).toBe(message);
}
};

View File

@ -0,0 +1,39 @@
// 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).toEqual(jasmine.any(PreviewServerError));
expect(err).toEqual(jasmine.any(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('');
});
});

View File

@ -0,0 +1,600 @@
// Imports
import * as express from 'express';
import * as http from 'http';
import * as supertest from 'supertest';
import {promisify} from 'util';
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 {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',
};
// Helpers
const createPreviewServer = (partialConfig: Partial<PreviewServerConfig> = {}) =>
PreviewServerFactory.create({...defaultConfig, ...partialConfig});
beforeEach(() => {
spyOn(console, 'error');
spyOn(console, 'info');
spyOn(console, '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: express.Express = 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(console.info).not.toHaveBeenCalled();
server.emit('listening');
expect(console.info).toHaveBeenCalledWith(
jasmine.any(String), 'PreviewServer: ', '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: GithubPullRequests = allCalls[0].object;
expect(prsAddCommentSpy).toHaveBeenCalledTimes(2);
expect(prs).toBe(allCalls[1].object);
expect(prs).toEqual(jasmine.any(GithubPullRequests));
expect(prs.repoSlug).toBe('organisation/repo');
});
});
describe('createMiddleware()', () => {
let buildRetriever: BuildRetriever;
let buildVerifier: BuildVerifier;
let buildCreator: BuildCreator;
let agent: supertest.SuperTest<supertest.Test>;
// Helpers
const promisifyRequest = async (req: supertest.Request) => await promisify(req.end.bind(req))();
const verifyRequests = async (reqs: supertest.Request[]) => await Promise.all(reqs.map(promisifyRequest));
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 verifyRequests([
agent.get('/health-check').expect(200),
agent.get('/health-check/').expect(200),
]);
});
it('should respond with 404 for non-GET requests', async () => {
await verifyRequests([
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 verifyRequests([
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('/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,
success: true,
};
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(console.log).toHaveBeenCalledWith(jasmine.any(String), 'PreviewServer: ',
'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(console.log).toHaveBeenCalledWith(jasmine.any(String), 'PreviewServer: ',
'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 not create a preview if the build was not successful', async () => {
BUILD_INFO.success = false;
await agent.post(URL).send(BASIC_PAYLOAD).expect(204);
expect(getGithubInfoSpy).toHaveBeenCalledWith(BUILD_NUM);
expect(downloadBuildArtifactSpy).not.toHaveBeenCalled();
expect(getPrIsTrustedSpy).not.toHaveBeenCalled();
expect(createBuildSpy).not.toHaveBeenCalled();
});
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.callFake(() => Promise.reject('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.callFake(() => Promise.reject('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.callFake(() => Promise.reject('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.callFake(() => Promise.reject('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 verifyRequests([
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 verifyRequests([
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 verifyRequests([
request1.expect(400, `${responseBodyPrefix} {}`),
request2.expect(400, `${responseBodyPrefix} {"number":null}`),
]);
});
it('should call \'BuildVerifier#gtPrIsTrusted()\' with the correct arguments', async () => {
await promisifyRequest(createRequest(+pr));
expect(bvGetPrIsTrustedSpy).toHaveBeenCalledWith(9);
});
it('should propagate errors from BuildVerifier', async () => {
bvGetPrIsTrustedSpy.and.callFake(() => Promise.reject('Test'));
const req = createRequest(+pr).expect(500, 'Test');
await promisifyRequest(req);
expect(bvGetPrIsTrustedSpy).toHaveBeenCalledWith(9);
expect(bcUpdatePrVisibilitySpy).not.toHaveBeenCalled();
});
it('should call \'BuildCreator#updatePrVisibility()\' with the correct arguments', async () => {
bvGetPrIsTrustedSpy.and.callFake((pr2: number) => Promise.resolve(pr2 === 42));
await promisifyRequest(createRequest(24));
expect(bcUpdatePrVisibilitySpy).toHaveBeenCalledWith(24, false);
await promisifyRequest(createRequest(42));
expect(bcUpdatePrVisibilitySpy).toHaveBeenCalledWith(42, true);
});
it('should propagate errors from BuildCreator', async () => {
bcUpdatePrVisibilitySpy.and.callFake(() => Promise.reject('Test'));
const req = createRequest(+pr).expect(500, 'Test');
await verifyRequests([req]);
});
describe('on success', () => {
it('should respond with 200 (action: undefined)', async () => {
bvGetPrIsTrustedSpy.and.returnValues(Promise.resolve(true), Promise.resolve(false));
const reqs = [4, 2].map(num => createRequest(num).expect(200, http.STATUS_CODES[200]));
await verifyRequests(reqs);
});
it('should respond with 200 (action: labeled)', async () => {
bvGetPrIsTrustedSpy.and.returnValues(Promise.resolve(true), Promise.resolve(false));
const reqs = [4, 2].map(num => createRequest(num, 'labeled').expect(200, http.STATUS_CODES[200]));
await verifyRequests(reqs);
});
it('should respond with 200 (action: unlabeled)', async () => {
bvGetPrIsTrustedSpy.and.returnValues(Promise.resolve(true), Promise.resolve(false));
const reqs = [4, 2].map(num => createRequest(num, 'unlabeled').expect(200, http.STATUS_CODES[200]));
await verifyRequests(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])).
map(promisifyRequest);
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 verifyRequests([
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')),
]);
});
});
});
});

View File

@ -0,0 +1,49 @@
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) {
caught = true;
expect(error).toEqual(jasmine.any(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);
});
});
});

View File

@ -1,303 +0,0 @@
// Imports
import * as jwt from 'jsonwebtoken';
import {GithubPullRequests, PullRequest} from '../../lib/common/github-pull-requests';
import {GithubTeams} from '../../lib/common/github-teams';
import {BUILD_VERIFICATION_STATUS, BuildVerifier} from '../../lib/upload-server/build-verifier';
import {expectToBeUploadError} from './helpers';
// Tests
describe('BuildVerifier', () => {
const defaultConfig = {
allowedTeamSlugs: ['team1', 'team2'],
githubToken: 'githubToken',
organization: 'organization',
repoSlug: 'repo/slug',
secret: 'secret',
trustedPrLabel: 'trusted: pr-label',
};
let bv: BuildVerifier;
// Helpers
const createBuildVerifier = (partialConfig: Partial<typeof defaultConfig> = {}) => {
const cfg = {...defaultConfig, ...partialConfig} as typeof defaultConfig;
return new BuildVerifier(cfg.secret, cfg.githubToken, cfg.repoSlug, cfg.organization,
cfg.allowedTeamSlugs, cfg.trustedPrLabel);
};
beforeEach(() => bv = createBuildVerifier());
describe('constructor()', () => {
['secret', 'githubToken', 'repoSlug', 'organization', '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('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.returnValue(Promise.resolve(mockPrInfo));
teamsIsMemberBySlugSpy = spyOn(GithubTeams.prototype, 'isMemberBySlug').
and.returnValue(Promise.resolve(true));
});
it('should return a promise', done => {
const promise = bv.getPrIsTrusted(pr);
promise.then(done); // Do not complete the test (and release the spies) synchronously
// to avoid running the actual `GithubTeams#isMemberBySlug()`.
expect(promise).toEqual(jasmine.any(Promise));
});
it('should fetch the corresponding PR', done => {
bv.getPrIsTrusted(pr).then(() => {
expect(prsFetchSpy).toHaveBeenCalledWith(pr);
done();
});
});
it('should fail if fetching the PR errors', done => {
prsFetchSpy.and.callFake(() => Promise.reject('Test'));
bv.getPrIsTrusted(pr).catch(err => {
expect(err).toBe('Test');
done();
});
});
describe('when the PR has the "trusted PR" label', () => {
beforeEach(() => mockPrInfo.labels.push({name: 'trusted: pr-label'}));
it('should resolve to true', done => {
bv.getPrIsTrusted(pr).then(isTrusted => {
expect(isTrusted).toBe(true);
done();
});
});
it('should not try to verify the author\'s membership status', done => {
bv.getPrIsTrusted(pr).then(() => {
expect(teamsIsMemberBySlugSpy).not.toHaveBeenCalled();
done();
});
});
});
describe('when the PR does not have the "trusted PR" label', () => {
it('should verify the PR author\'s membership in the specified teams', done => {
bv.getPrIsTrusted(pr).then(() => {
expect(teamsIsMemberBySlugSpy).toHaveBeenCalledWith('username', ['team1', 'team2']);
done();
});
});
it('should fail if verifying membership errors', done => {
teamsIsMemberBySlugSpy.and.callFake(() => Promise.reject('Test'));
bv.getPrIsTrusted(pr).catch(err => {
expect(err).toBe('Test');
done();
});
});
it('should resolve to true if the PR\'s author is a member', done => {
teamsIsMemberBySlugSpy.and.returnValue(Promise.resolve(true));
bv.getPrIsTrusted(pr).then(isTrusted => {
expect(isTrusted).toBe(true);
done();
});
});
it('should resolve to false if the PR\'s author is not a member', done => {
teamsIsMemberBySlugSpy.and.returnValue(Promise.resolve(false));
bv.getPrIsTrusted(pr).then(isTrusted => {
expect(isTrusted).toBe(false);
done();
});
});
});
});
describe('verify()', () => {
const pr = 9;
const defaultJwt = {
'exp': Math.floor(Date.now() / 1000) + 30,
'iat': Math.floor(Date.now() / 1000) - 30,
'iss': 'Travis CI, GmbH',
'pull-request': pr,
'slug': defaultConfig.repoSlug,
};
let bvGetPrIsTrusted: jasmine.Spy;
// Heleprs
const createAuthHeader = (partialJwt: Partial<typeof defaultJwt> = {}, secret: string = defaultConfig.secret) =>
`Token ${jwt.sign({...defaultJwt, ...partialJwt}, secret)}`;
beforeEach(() => {
bvGetPrIsTrusted = spyOn(bv, 'getPrIsTrusted').and.returnValue(Promise.resolve(true));
});
it('should return a promise', done => {
const promise = bv.verify(pr, createAuthHeader());
promise.then(done); // Do not complete the test (and release the spies) synchronously
// to avoid running the actual `bvGetPrIsTrusted()`.
expect(promise).toEqual(jasmine.any(Promise));
});
it('should fail if the authorization header is invalid', done => {
bv.verify(pr, 'foo').catch(err => {
const errorMessage = 'Error while verifying upload for PR 9: jwt malformed';
expectToBeUploadError(err, 403, errorMessage);
done();
});
});
it('should fail if the secret is invalid', done => {
bv.verify(pr, createAuthHeader({}, 'foo')).catch(err => {
const errorMessage = 'Error while verifying upload for PR 9: invalid signature';
expectToBeUploadError(err, 403, errorMessage);
done();
});
});
it('should fail if the issuer is invalid', done => {
bv.verify(pr, createAuthHeader({iss: 'not valid'})).catch(err => {
const errorMessage = 'Error while verifying upload for PR 9: ' +
`jwt issuer invalid. expected: ${defaultJwt.iss}`;
expectToBeUploadError(err, 403, errorMessage);
done();
});
});
it('should fail if the token has expired', done => {
bv.verify(pr, createAuthHeader({exp: 0})).catch(err => {
const errorMessage = 'Error while verifying upload for PR 9: jwt expired';
expectToBeUploadError(err, 403, errorMessage);
done();
});
});
it('should fail if the repo slug does not match', done => {
bv.verify(pr, createAuthHeader({slug: 'foo/bar'})).catch(err => {
const errorMessage = 'Error while verifying upload for PR 9: ' +
`jwt slug invalid. expected: ${defaultConfig.repoSlug}`;
expectToBeUploadError(err, 403, errorMessage);
done();
});
});
it('should fail if the PR does not match', done => {
bv.verify(pr, createAuthHeader({'pull-request': 1337})).catch(err => {
const errorMessage = 'Error while verifying upload for PR 9: ' +
`jwt pull-request invalid. expected: ${pr}`;
expectToBeUploadError(err, 403, errorMessage);
done();
});
});
it('should not fail if the token is valid', done => {
bv.verify(pr, createAuthHeader()).then(done);
});
it('should not fail even if the token has been issued in the future', done => {
const in30s = Math.floor(Date.now() / 1000) + 30;
bv.verify(pr, createAuthHeader({iat: in30s})).then(done);
});
it('should call \'getPrIsTrusted()\' if the token is valid', done => {
bv.verify(pr, createAuthHeader()).then(() => {
expect(bvGetPrIsTrusted).toHaveBeenCalledWith(pr);
done();
});
});
it('should fail if \'getPrIsTrusted()\' rejects', done => {
bvGetPrIsTrusted.and.callFake(() => Promise.reject('Test'));
bv.verify(pr, createAuthHeader()).catch(err => {
expectToBeUploadError(err, 403, `Error while verifying upload for PR ${pr}: Test`);
done();
});
});
it('should resolve to `verifiedNotTrusted` if \'getPrIsTrusted()\' returns false', done => {
bvGetPrIsTrusted.and.returnValue(Promise.resolve(false));
bv.verify(pr, createAuthHeader()).then(value => {
expect(value).toBe(BUILD_VERIFICATION_STATUS.verifiedNotTrusted);
done();
});
});
it('should resolve to `verifiedAndTrusted` if \'getPrIsTrusted()\' returns true', done => {
bv.verify(pr, createAuthHeader()).then(value => {
expect(value).toBe(BUILD_VERIFICATION_STATUS.verifiedAndTrusted);
done();
});
});
});
});

View File

@ -1,11 +0,0 @@
import {UploadError} from '../../lib/upload-server/upload-error';
export const expectToBeUploadError = (actual: UploadError, status?: number, message?: string) => {
expect(actual).toEqual(jasmine.any(UploadError));
if (status != null) {
expect(actual.status).toBe(status);
}
if (message != null) {
expect(actual.message).toBe(message);
}
};

View File

@ -1,39 +0,0 @@
// Imports
import {UploadError} from '../../lib/upload-server/upload-error';
// Tests
describe('UploadError', () => {
let err: UploadError;
beforeEach(() => err = new UploadError(999, 'message'));
it('should extend Error', () => {
expect(err).toEqual(jasmine.any(UploadError));
expect(err).toEqual(jasmine.any(Error));
expect(Object.getPrototypeOf(err)).toBe(UploadError.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 UploadError().status).toBe(500);
});
it('should have an empty \'message\' by default', () => {
expect(new UploadError().message).toBe('');
expect(new UploadError(999).message).toBe('');
});
});

View File

@ -1,603 +0,0 @@
// Imports
import * as express from 'express';
import * as http from 'http';
import * as supertest from 'supertest';
import {GithubPullRequests} from '../../lib/common/github-pull-requests';
import {BuildCreator} from '../../lib/upload-server/build-creator';
import {ChangedPrVisibilityEvent, CreatedBuildEvent} from '../../lib/upload-server/build-events';
import {BUILD_VERIFICATION_STATUS, BuildVerifier} from '../../lib/upload-server/build-verifier';
import {uploadServerFactory as usf} from '../../lib/upload-server/upload-server-factory';
// Tests
describe('uploadServerFactory', () => {
const defaultConfig = {
buildsDir: 'builds/dir',
domainName: 'domain.name',
githubOrganization: 'organization',
githubTeamSlugs: ['team1', 'team2'],
githubToken: '12345',
repoSlug: 'repo/slug',
secret: 'secret',
trustedPrLabel: 'trusted: pr-label',
};
// Helpers
const createUploadServer = (partialConfig: Partial<typeof defaultConfig> = {}) =>
usf.create({...defaultConfig, ...partialConfig} as typeof defaultConfig);
describe('create()', () => {
let usfCreateMiddlewareSpy: jasmine.Spy;
beforeEach(() => {
usfCreateMiddlewareSpy = spyOn(usf as any, 'createMiddleware').and.callThrough();
});
it('should throw if \'buildsDir\' is missing or empty', () => {
expect(() => createUploadServer({buildsDir: ''})).
toThrowError('Missing or empty required parameter \'buildsDir\'!');
});
it('should throw if \'domainName\' is missing or empty', () => {
expect(() => createUploadServer({domainName: ''})).
toThrowError('Missing or empty required parameter \'domainName\'!');
});
it('should throw if \'githubToken\' is missing or empty', () => {
expect(() => createUploadServer({githubToken: ''})).
toThrowError('Missing or empty required parameter \'githubToken\'!');
});
it('should throw if \'githubOrganization\' is missing or empty', () => {
expect(() => createUploadServer({githubOrganization: ''})).
toThrowError('Missing or empty required parameter \'organization\'!');
});
it('should throw if \'githubTeamSlugs\' is missing or empty', () => {
expect(() => createUploadServer({githubTeamSlugs: []})).
toThrowError('Missing or empty required parameter \'allowedTeamSlugs\'!');
});
it('should throw if \'repoSlug\' is missing or empty', () => {
expect(() => createUploadServer({repoSlug: ''})).
toThrowError('Missing or empty required parameter \'repoSlug\'!');
});
it('should throw if \'secret\' is missing or empty', () => {
expect(() => createUploadServer({secret: ''})).
toThrowError('Missing or empty required parameter \'secret\'!');
});
it('should throw if \'trustedPrLabel\' is missing or empty', () => {
expect(() => createUploadServer({trustedPrLabel: ''})).
toThrowError('Missing or empty required parameter \'trustedPrLabel\'!');
});
it('should return an http.Server', () => {
const httpCreateServerSpy = spyOn(http, 'createServer').and.callThrough();
const server = createUploadServer();
expect(server).toBe(httpCreateServerSpy.calls.mostRecent().returnValue);
});
it('should create and use an appropriate BuildCreator', () => {
const usfCreateBuildCreatorSpy = spyOn(usf as any, 'createBuildCreator').and.callThrough();
createUploadServer();
const buildCreator: BuildCreator = usfCreateBuildCreatorSpy.calls.mostRecent().returnValue;
expect(usfCreateMiddlewareSpy).toHaveBeenCalledWith(jasmine.any(BuildVerifier), buildCreator);
expect(usfCreateBuildCreatorSpy).toHaveBeenCalledWith('builds/dir', '12345', 'repo/slug', 'domain.name');
});
it('should create and use an appropriate middleware', () => {
const httpCreateServerSpy = spyOn(http, 'createServer').and.callThrough();
createUploadServer();
const middleware: express.Express = usfCreateMiddlewareSpy.calls.mostRecent().returnValue;
const buildVerifier = jasmine.any(BuildVerifier);
const buildCreator = jasmine.any(BuildCreator);
expect(httpCreateServerSpy).toHaveBeenCalledWith(middleware);
expect(usfCreateMiddlewareSpy).toHaveBeenCalledWith(buildVerifier, buildCreator);
});
it('should log the server address info on \'listening\'', () => {
const consoleInfoSpy = spyOn(console, 'info');
const server = createUploadServer();
server.address = () => ({address: 'foo', family: '', port: 1337});
expect(consoleInfoSpy).not.toHaveBeenCalled();
server.emit('listening');
expect(consoleInfoSpy).toHaveBeenCalledWith('Up and running (and listening on foo:1337)...');
});
});
// Protected methods
describe('createBuildCreator()', () => {
let buildCreator: BuildCreator;
beforeEach(() => {
buildCreator = (usf as any).createBuildCreator(
defaultConfig.buildsDir,
defaultConfig.githubToken,
defaultConfig.repoSlug,
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 \'githubToken\' and \'repoSlug\' 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;
expect(prsAddCommentSpy).toHaveBeenCalledTimes(2);
expect(prs).toBe(allCalls[1].object);
expect(prs).toEqual(jasmine.any(GithubPullRequests));
expect(prs.repoSlug).toBe('repo/slug');
expect(prs.requestHeaders.Authorization).toContain('12345');
});
});
describe('createMiddleware()', () => {
let buildVerifier: BuildVerifier;
let buildCreator: BuildCreator;
let agent: supertest.SuperTest<supertest.Test>;
// Helpers
const promisifyRequest = (req: supertest.Request) =>
new Promise((resolve, reject) => req.end(err => err ? reject(err) : resolve()));
const verifyRequests = (reqs: supertest.Request[], done: jasmine.DoneFn) =>
Promise.all(reqs.map(promisifyRequest)).then(done, done.fail);
beforeEach(() => {
buildVerifier = new BuildVerifier(
defaultConfig.secret,
defaultConfig.githubToken,
defaultConfig.repoSlug,
defaultConfig.githubOrganization,
defaultConfig.githubTeamSlugs,
defaultConfig.trustedPrLabel,
);
buildCreator = new BuildCreator(defaultConfig.buildsDir);
agent = supertest.agent((usf as any).createMiddleware(buildVerifier, buildCreator));
spyOn(console, 'error');
});
describe('GET /create-build/<pr>/<sha>', () => {
const pr = '9';
const sha = '9'.repeat(40);
let buildVerifierVerifySpy: jasmine.Spy;
let buildCreatorCreateSpy: jasmine.Spy;
beforeEach(() => {
const verStatus = BUILD_VERIFICATION_STATUS.verifiedAndTrusted;
buildVerifierVerifySpy = spyOn(buildVerifier, 'verify').and.returnValue(Promise.resolve(verStatus));
buildCreatorCreateSpy = spyOn(buildCreator, 'create').and.returnValue(Promise.resolve());
});
it('should respond with 404 for non-GET requests', done => {
verifyRequests([
agent.put(`/create-build/${pr}/${sha}`).expect(404),
agent.post(`/create-build/${pr}/${sha}`).expect(404),
agent.patch(`/create-build/${pr}/${sha}`).expect(404),
agent.delete(`/create-build/${pr}/${sha}`).expect(404),
], done);
});
it('should respond with 401 for requests without an \'AUTHORIZATION\' header', done => {
const url = `/create-build/${pr}/${sha}`;
const responseBody = `Missing or empty 'AUTHORIZATION' header in request: GET ${url}`;
verifyRequests([
agent.get(url).expect(401, responseBody),
agent.get(url).set('AUTHORIZATION', '').expect(401, responseBody),
], done);
});
it('should respond with 400 for requests without an \'X-FILE\' header', done => {
const url = `/create-build/${pr}/${sha}`;
const responseBody = `Missing or empty 'X-FILE' header in request: GET ${url}`;
const request1 = agent.get(url).set('AUTHORIZATION', 'foo');
const request2 = agent.get(url).set('AUTHORIZATION', 'foo').set('X-FILE', '');
verifyRequests([
request1.expect(400, responseBody),
request2.expect(400, responseBody),
], done);
});
it('should respond with 404 for unknown paths', done => {
verifyRequests([
agent.get(`/foo/create-build/${pr}/${sha}`).expect(404),
agent.get(`/foo-create-build/${pr}/${sha}`).expect(404),
agent.get(`/fooncreate-build/${pr}/${sha}`).expect(404),
agent.get(`/create-build/foo/${pr}/${sha}`).expect(404),
agent.get(`/create-build-foo/${pr}/${sha}`).expect(404),
agent.get(`/create-buildnfoo/${pr}/${sha}`).expect(404),
agent.get(`/create-build/pr${pr}/${sha}`).expect(404),
agent.get(`/create-build/${pr}/${sha}42`).expect(404),
], done);
});
it('should call \'BuildVerifier#verify()\' with the correct arguments', done => {
const req = agent.
get(`/create-build/${pr}/${sha}`).
set('AUTHORIZATION', 'foo').
set('X-FILE', 'bar');
promisifyRequest(req).
then(() => expect(buildVerifierVerifySpy).toHaveBeenCalledWith(9, 'foo')).
then(done, done.fail);
});
it('should propagate errors from BuildVerifier', done => {
buildVerifierVerifySpy.and.callFake(() => Promise.reject('Test'));
const req = agent.
get(`/create-build/${pr}/${sha}`).
set('AUTHORIZATION', 'foo').
set('X-FILE', 'bar').
expect(500, 'Test');
promisifyRequest(req).
then(() => {
expect(buildVerifierVerifySpy).toHaveBeenCalledWith(9, 'foo');
expect(buildCreatorCreateSpy).not.toHaveBeenCalled();
}).
then(done, done.fail);
});
it('should call \'BuildCreator#create()\' with the correct arguments', done => {
buildVerifierVerifySpy.and.returnValues(
Promise.resolve(BUILD_VERIFICATION_STATUS.verifiedAndTrusted),
Promise.resolve(BUILD_VERIFICATION_STATUS.verifiedNotTrusted));
const req1 = agent.get(`/create-build/${pr}/${sha}`).set('AUTHORIZATION', 'foo').set('X-FILE', 'bar');
const req2 = agent.get(`/create-build/${pr}/${sha}`).set('AUTHORIZATION', 'foo').set('X-FILE', 'bar');
Promise.all([
promisifyRequest(req1).then(() => expect(buildCreatorCreateSpy).toHaveBeenCalledWith(pr, sha, 'bar', true)),
promisifyRequest(req2).then(() => expect(buildCreatorCreateSpy).toHaveBeenCalledWith(pr, sha, 'bar', false)),
]).then(done, done.fail);
});
it('should propagate errors from BuildCreator', done => {
buildCreatorCreateSpy.and.callFake(() => Promise.reject('Test'));
const req = agent.
get(`/create-build/${pr}/${sha}`).
set('AUTHORIZATION', 'foo').
set('X-FILE', 'bar').
expect(500, 'Test');
verifyRequests([req], done);
});
it('should respond with 201 on successful upload (for public builds)', done => {
const req = agent.
get(`/create-build/${pr}/${sha}`).
set('AUTHORIZATION', 'foo').
set('X-FILE', 'bar').
expect(201, http.STATUS_CODES[201]);
verifyRequests([req], done);
});
it('should respond with 202 on successful upload (for hidden builds)', done => {
buildVerifierVerifySpy.and.returnValue(Promise.resolve(BUILD_VERIFICATION_STATUS.verifiedNotTrusted));
const req = agent.
get(`/create-build/${pr}/${sha}`).
set('AUTHORIZATION', 'foo').
set('X-FILE', 'bar').
expect(202, http.STATUS_CODES[202]);
verifyRequests([req], done);
});
it('should reject PRs with leading zeros', done => {
verifyRequests([agent.get(`/create-build/0${pr}/${sha}`).expect(404)], done);
});
it('should accept SHAs with leading zeros (but not trim the zeros)', done => {
const sha40 = '0'.repeat(40);
const sha41 = `0${sha40}`;
const request40 = agent.get(`/create-build/${pr}/${sha40}`).set('AUTHORIZATION', 'foo').set('X-FILE', 'bar');
const request41 = agent.get(`/create-build/${pr}/${sha41}`).set('AUTHORIZATION', 'baz').set('X-FILE', 'qux');
Promise.all([
promisifyRequest(request40.expect(201)),
promisifyRequest(request41.expect(404)),
]).then(done, done.fail);
});
});
describe('GET /health-check', () => {
it('should respond with 200', done => {
verifyRequests([
agent.get('/health-check').expect(200),
agent.get('/health-check/').expect(200),
], done);
});
it('should respond with 404 for non-GET requests', done => {
verifyRequests([
agent.put('/health-check').expect(404),
agent.post('/health-check').expect(404),
agent.patch('/health-check').expect(404),
agent.delete('/health-check').expect(404),
], done);
});
it('should respond with 404 if the path does not match exactly', done => {
verifyRequests([
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),
], done);
});
});
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', done => {
verifyRequests([
agent.get(url).expect(404),
agent.put(url).expect(404),
agent.patch(url).expect(404),
agent.delete(url).expect(404),
], done);
});
it('should respond with 400 for requests without a payload', done => {
const responseBody = `Missing or empty 'number' field in request: POST ${url} {}`;
const request1 = agent.post(url);
const request2 = agent.post(url).send();
verifyRequests([
request1.expect(400, responseBody),
request2.expect(400, responseBody),
], done);
});
it('should respond with 400 for requests without a \'number\' field', done => {
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});
verifyRequests([
request1.expect(400, `${responseBodyPrefix} {}`),
request2.expect(400, `${responseBodyPrefix} {"number":null}`),
], done);
});
it('should call \'BuildVerifier#gtPrIsTrusted()\' with the correct arguments', done => {
const req = createRequest(+pr);
promisifyRequest(req).
then(() => expect(bvGetPrIsTrustedSpy).toHaveBeenCalledWith(9)).
then(done, done.fail);
});
it('should propagate errors from BuildVerifier', done => {
bvGetPrIsTrustedSpy.and.callFake(() => Promise.reject('Test'));
const req = createRequest(+pr).expect(500, 'Test');
promisifyRequest(req).
then(() => {
expect(bvGetPrIsTrustedSpy).toHaveBeenCalledWith(9);
expect(bcUpdatePrVisibilitySpy).not.toHaveBeenCalled();
}).
then(done, done.fail);
});
it('should call \'BuildCreator#updatePrVisibility()\' with the correct arguments', done => {
bvGetPrIsTrustedSpy.and.callFake((pr2: number) => Promise.resolve(pr2 === 42));
const req1 = createRequest(24);
const req2 = createRequest(42);
Promise.all([
promisifyRequest(req1).then(() => expect(bcUpdatePrVisibilitySpy).toHaveBeenCalledWith('24', false)),
promisifyRequest(req2).then(() => expect(bcUpdatePrVisibilitySpy).toHaveBeenCalledWith('42', true)),
]).then(done, done.fail);
});
it('should propagate errors from BuildCreator', done => {
bcUpdatePrVisibilitySpy.and.callFake(() => Promise.reject('Test'));
const req = createRequest(+pr).expect(500, 'Test');
verifyRequests([req], done);
});
describe('on success', () => {
it('should respond with 200 (action: undefined)', done => {
bvGetPrIsTrustedSpy.and.returnValues(Promise.resolve(true), Promise.resolve(false));
const reqs = [4, 2].map(num => createRequest(num).expect(200, http.STATUS_CODES[200]));
verifyRequests(reqs, done);
});
it('should respond with 200 (action: labeled)', done => {
bvGetPrIsTrustedSpy.and.returnValues(Promise.resolve(true), Promise.resolve(false));
const reqs = [4, 2].map(num => createRequest(num, 'labeled').expect(200, http.STATUS_CODES[200]));
verifyRequests(reqs, done);
});
it('should respond with 200 (action: unlabeled)', done => {
bvGetPrIsTrustedSpy.and.returnValues(Promise.resolve(true), Promise.resolve(false));
const reqs = [4, 2].map(num => createRequest(num, 'unlabeled').expect(200, http.STATUS_CODES[200]));
verifyRequests(reqs, done);
});
it('should respond with 200 (and do nothing) if \'action\' implies no visibility change', done => {
const promises = ['foo', 'notlabeled'].
map(action => createRequest(+pr, action).expect(200, http.STATUS_CODES[200])).
map(promisifyRequest);
Promise.all(promises).
then(() => {
expect(bvGetPrIsTrustedSpy).not.toHaveBeenCalled();
expect(bcUpdatePrVisibilitySpy).not.toHaveBeenCalled();
}).
then(done, done.fail);
});
});
});
describe('ALL *', () => {
it('should respond with 404', done => {
const responseFor = (method: string) => `Unknown resource in request: ${method.toUpperCase()} /some/url`;
verifyRequests([
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')),
], done);
});
});
});
});

View File

@ -40,12 +40,6 @@
version "2.6.0" version "2.6.0"
resolved "https://registry.yarnpkg.com/@types/jasmine/-/jasmine-2.6.0.tgz#997b41a27752b4850af2683bc4a8d8222c25bd02" resolved "https://registry.yarnpkg.com/@types/jasmine/-/jasmine-2.6.0.tgz#997b41a27752b4850af2683bc4a8d8222c25bd02"
"@types/jsonwebtoken@^7.2.3":
version "7.2.3"
resolved "https://registry.yarnpkg.com/@types/jsonwebtoken/-/jsonwebtoken-7.2.3.tgz#483c8f39945e1e6d308dcc51fd4aeca5208d4dca"
dependencies:
"@types/node" "*"
"@types/mime@*": "@types/mime@*":
version "1.3.0" version "1.3.0"
resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.0.tgz#d24ffac7d1006fe68517202fb2aeba3dbe48284b" resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.0.tgz#d24ffac7d1006fe68517202fb2aeba3dbe48284b"
@ -54,6 +48,18 @@
version "3.0.1" version "3.0.1"
resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.1.tgz#b683eb60be358304ef146f5775db4c0e3696a550" resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.1.tgz#b683eb60be358304ef146f5775db4c0e3696a550"
"@types/nock@^9.1.3":
version "9.1.3"
resolved "https://registry.yarnpkg.com/@types/nock/-/nock-9.1.3.tgz#1d445679375b9e25afd449dc56585f81729454e8"
dependencies:
"@types/node" "*"
"@types/node-fetch@^1.6.8":
version "1.6.8"
resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-1.6.8.tgz#a59d8c75b300ddc3ca3eef23d449d677f9486c3d"
dependencies:
"@types/node" "*"
"@types/node@*": "@types/node@*":
version "7.0.31" version "7.0.31"
resolved "https://registry.yarnpkg.com/@types/node/-/node-7.0.31.tgz#80ea4d175599b2a00149c29a10a4eb2dff592e86" resolved "https://registry.yarnpkg.com/@types/node/-/node-7.0.31.tgz#80ea4d175599b2a00149c29a10a4eb2dff592e86"
@ -69,9 +75,9 @@
"@types/express-serve-static-core" "*" "@types/express-serve-static-core" "*"
"@types/mime" "*" "@types/mime" "*"
"@types/shelljs@^0.7.4": "@types/shelljs@^0.8.0":
version "0.7.4" version "0.8.0"
resolved "https://registry.yarnpkg.com/@types/shelljs/-/shelljs-0.7.4.tgz#137b5f31306eaff4de120ffe5b9d74b297809cfc" resolved "https://registry.yarnpkg.com/@types/shelljs/-/shelljs-0.8.0.tgz#0caa56b68baae4f68f44e0dd666ab30b098e3632"
dependencies: dependencies:
"@types/glob" "*" "@types/glob" "*"
"@types/node" "*" "@types/node" "*"
@ -112,6 +118,12 @@ ansi-align@^2.0.0:
dependencies: dependencies:
string-width "^2.0.0" string-width "^2.0.0"
ansi-green@^0.1.1:
version "0.1.1"
resolved "https://registry.yarnpkg.com/ansi-green/-/ansi-green-0.1.1.tgz#8a5d9a979e458d57c40e33580b37390b8e10d0f7"
dependencies:
ansi-wrap "0.1.0"
ansi-regex@^0.2.0, ansi-regex@^0.2.1: ansi-regex@^0.2.0, ansi-regex@^0.2.1:
version "0.2.1" version "0.2.1"
resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-0.2.1.tgz#0d8e946967a3d8143f93e24e298525fc1b2235f9" resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-0.2.1.tgz#0d8e946967a3d8143f93e24e298525fc1b2235f9"
@ -128,6 +140,10 @@ ansi-styles@^2.2.1:
version "2.2.1" version "2.2.1"
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe"
ansi-wrap@0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/ansi-wrap/-/ansi-wrap-0.1.0.tgz#a82250ddb0015e9a27ca82e82ea603bbfa45efaf"
anymatch@^1.3.0: anymatch@^1.3.0:
version "1.3.0" version "1.3.0"
resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-1.3.0.tgz#a3e52fa39168c825ff57b0248126ce5a8ff95507" resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-1.3.0.tgz#a3e52fa39168c825ff57b0248126ce5a8ff95507"
@ -180,6 +196,10 @@ assert-plus@^0.2.0:
version "0.2.0" version "0.2.0"
resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-0.2.0.tgz#d74e1b87e7affc0db8aadb7021f3fe48101ab234" resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-0.2.0.tgz#d74e1b87e7affc0db8aadb7021f3fe48101ab234"
assertion-error@^1.0.1:
version "1.1.0"
resolved "https://registry.yarnpkg.com/assertion-error/-/assertion-error-1.1.0.tgz#e60b6b0e8f301bd97e5375215bda406c85118c0b"
async-each@^1.0.0: async-each@^1.0.0:
version "1.0.1" version "1.0.1"
resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.1.tgz#19d386a1d9edc6e7c1c85d388aedbcc56d33602d" resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.1.tgz#19d386a1d9edc6e7c1c85d388aedbcc56d33602d"
@ -208,10 +228,6 @@ balanced-match@^1.0.0:
version "1.0.0" version "1.0.0"
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767"
base64url@2.0.0, base64url@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/base64url/-/base64url-2.0.0.tgz#eac16e03ea1438eff9423d69baa36262ed1f70bb"
bcrypt-pbkdf@^1.0.0: bcrypt-pbkdf@^1.0.0:
version "1.0.1" version "1.0.1"
resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz#63bc5dcb61331b92bc05fd528953c33462a06f8d" resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz#63bc5dcb61331b92bc05fd528953c33462a06f8d"
@ -222,6 +238,13 @@ binary-extensions@^1.0.0:
version "1.8.0" version "1.8.0"
resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.8.0.tgz#48ec8d16df4377eae5fa5884682480af4d95c774" resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.8.0.tgz#48ec8d16df4377eae5fa5884682480af4d95c774"
bl@^1.0.0:
version "1.2.2"
resolved "https://registry.yarnpkg.com/bl/-/bl-1.2.2.tgz#a160911717103c07410cef63ef51b397c025af9c"
dependencies:
readable-stream "^2.3.5"
safe-buffer "^5.1.1"
block-stream@*: block-stream@*:
version "0.0.9" version "0.0.9"
resolved "https://registry.yarnpkg.com/block-stream/-/block-stream-0.0.9.tgz#13ebfe778a03205cfe03751481ebb4b3300c126a" resolved "https://registry.yarnpkg.com/block-stream/-/block-stream-0.0.9.tgz#13ebfe778a03205cfe03751481ebb4b3300c126a"
@ -276,9 +299,20 @@ braces@^1.8.2:
preserve "^0.2.0" preserve "^0.2.0"
repeat-element "^1.1.2" repeat-element "^1.1.2"
buffer-equal-constant-time@1.0.1: buffer-alloc-unsafe@^0.1.0:
version "1.0.1" version "0.1.1"
resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819" resolved "https://registry.yarnpkg.com/buffer-alloc-unsafe/-/buffer-alloc-unsafe-0.1.1.tgz#ffe1f67551dd055737de253337bfe853dfab1a6a"
buffer-alloc@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/buffer-alloc/-/buffer-alloc-1.1.0.tgz#05514d33bf1656d3540c684f65b1202e90eca303"
dependencies:
buffer-alloc-unsafe "^0.1.0"
buffer-fill "^0.1.0"
buffer-fill@^0.1.0:
version "0.1.1"
resolved "https://registry.yarnpkg.com/buffer-fill/-/buffer-fill-0.1.1.tgz#76d825c4d6e50e06b7a31eb520c04d08cc235071"
bytes@3.0.0: bytes@3.0.0:
version "3.0.0" version "3.0.0"
@ -296,6 +330,17 @@ caseless@~0.12.0:
version "0.12.0" version "0.12.0"
resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc"
chai@^4.1.2:
version "4.1.2"
resolved "https://registry.yarnpkg.com/chai/-/chai-4.1.2.tgz#0f64584ba642f0f2ace2806279f4f06ca23ad73c"
dependencies:
assertion-error "^1.0.1"
check-error "^1.0.1"
deep-eql "^3.0.0"
get-func-name "^2.0.0"
pathval "^1.0.0"
type-detect "^4.0.0"
chalk@0.5.1: chalk@0.5.1:
version "0.5.1" version "0.5.1"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-0.5.1.tgz#663b3a648b68b55d04690d49167aa837858f2174" resolved "https://registry.yarnpkg.com/chalk/-/chalk-0.5.1.tgz#663b3a648b68b55d04690d49167aa837858f2174"
@ -316,6 +361,10 @@ chalk@^1.0.0, chalk@^1.1.1, chalk@^1.1.3:
strip-ansi "^3.0.0" strip-ansi "^3.0.0"
supports-color "^2.0.0" supports-color "^2.0.0"
check-error@^1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/check-error/-/check-error-1.0.2.tgz#574d312edd88bb5dd8912e9286dd6c0aed4aac82"
chokidar@^1.7.0: chokidar@^1.7.0:
version "1.7.0" version "1.7.0"
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-1.7.0.tgz#798e689778151c8076b4b360e5edd28cda2bb468" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-1.7.0.tgz#798e689778151c8076b4b360e5edd28cda2bb468"
@ -476,6 +525,22 @@ debug@^2.2.0:
dependencies: dependencies:
ms "2.0.0" ms "2.0.0"
debug@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261"
dependencies:
ms "2.0.0"
deep-eql@^3.0.0:
version "3.0.1"
resolved "https://registry.yarnpkg.com/deep-eql/-/deep-eql-3.0.1.tgz#dfc9404400ad1c8fe023e7da1df1c147c4b444df"
dependencies:
type-detect "^4.0.0"
deep-equal@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.0.1.tgz#f5d260292b660e084eff4cdbc9f08ad3247448b5"
deep-extend@~0.4.0: deep-extend@~0.4.0:
version "0.4.2" version "0.4.2"
resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.4.2.tgz#48b699c27e334bf89f10892be432f6e4c7d34a7f" resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.4.2.tgz#48b699c27e334bf89f10892be432f6e4c7d34a7f"
@ -488,6 +553,14 @@ delegates@^1.0.0:
version "1.0.0" version "1.0.0"
resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a"
delete-empty@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/delete-empty/-/delete-empty-2.0.0.tgz#dcf7c4f93a98445119acd57b137d13e7af78fa39"
dependencies:
log-ok "^0.1.1"
relative "^3.0.2"
rimraf "^2.6.2"
depd@1.1.1, depd@~1.1.1: depd@1.1.1, depd@~1.1.1:
version "1.1.1" version "1.1.1"
resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.1.tgz#5783b4e1c459f06fa5ca27f991f3d06e7a310359" resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.1.tgz#5783b4e1c459f06fa5ca27f991f3d06e7a310359"
@ -520,13 +593,6 @@ ecc-jsbn@~0.1.1:
dependencies: dependencies:
jsbn "~0.1.0" jsbn "~0.1.0"
ecdsa-sig-formatter@1.0.9:
version "1.0.9"
resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.9.tgz#4bc926274ec3b5abb5016e7e1d60921ac262b2a1"
dependencies:
base64url "^2.0.0"
safe-buffer "^5.0.1"
ee-first@1.1.1: ee-first@1.1.1:
version "1.1.1" version "1.1.1"
resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
@ -535,6 +601,12 @@ encodeurl@~1.0.1:
version "1.0.1" version "1.0.1"
resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.1.tgz#79e3d58655346909fe6f0f45a5de68103b294d20" resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.1.tgz#79e3d58655346909fe6f0f45a5de68103b294d20"
end-of-stream@^1.0.0:
version "1.4.1"
resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.1.tgz#ed29634d19baba463b6ce6b80a37213eab71ec43"
dependencies:
once "^1.4.0"
es6-promise@^3.3.1: es6-promise@^3.3.1:
version "3.3.1" version "3.3.1"
resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-3.3.1.tgz#a08cdde84ccdbf34d027a1451bc91d4bcd28a613" resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-3.3.1.tgz#a08cdde84ccdbf34d027a1451bc91d4bcd28a613"
@ -713,6 +785,10 @@ from@~0:
version "0.1.7" version "0.1.7"
resolved "https://registry.yarnpkg.com/from/-/from-0.1.7.tgz#83c60afc58b9c56997007ed1a768b3ab303a44fe" resolved "https://registry.yarnpkg.com/from/-/from-0.1.7.tgz#83c60afc58b9c56997007ed1a768b3ab303a44fe"
fs-constants@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad"
fs.realpath@^1.0.0: fs.realpath@^1.0.0:
version "1.0.0" version "1.0.0"
resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
@ -754,6 +830,10 @@ gauge@~2.7.3:
strip-ansi "^3.0.1" strip-ansi "^3.0.1"
wide-align "^1.1.0" wide-align "^1.1.0"
get-func-name@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/get-func-name/-/get-func-name-2.0.0.tgz#ead774abee72e20409433a066366023dd6887a41"
get-stream@^3.0.0: get-stream@^3.0.0:
version "3.0.0" version "3.0.0"
resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-3.0.0.tgz#8e943d1358dc37555054ecbe2edb05aa174ede14" resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-3.0.0.tgz#8e943d1358dc37555054ecbe2edb05aa174ede14"
@ -892,7 +972,7 @@ inflight@^1.0.4:
once "^1.3.0" once "^1.3.0"
wrappy "1" wrappy "1"
inherits@2, inherits@2.0.3, inherits@^2.0.1, inherits@~2.0.0, inherits@~2.0.1: inherits@2, inherits@2.0.3, inherits@^2.0.1, inherits@~2.0.0, inherits@~2.0.1, inherits@~2.0.3:
version "2.0.3" version "2.0.3"
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de"
@ -1044,7 +1124,7 @@ json-stable-stringify@^1.0.1:
dependencies: dependencies:
jsonify "~0.0.0" jsonify "~0.0.0"
json-stringify-safe@~5.0.1: json-stringify-safe@^5.0.1, json-stringify-safe@~5.0.1:
version "5.0.1" version "5.0.1"
resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb"
@ -1052,21 +1132,6 @@ jsonify@~0.0.0:
version "0.0.0" version "0.0.0"
resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73" resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73"
jsonwebtoken@^8.0.1:
version "8.0.1"
resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-8.0.1.tgz#50daef8d0a8c7de2cd06bc1013b75b04ccf3f0cf"
dependencies:
jws "^3.1.4"
lodash.includes "^4.3.0"
lodash.isboolean "^3.0.3"
lodash.isinteger "^4.0.4"
lodash.isnumber "^3.0.3"
lodash.isplainobject "^4.0.6"
lodash.isstring "^4.0.1"
lodash.once "^4.0.0"
ms "^2.0.0"
xtend "^4.0.1"
jsprim@^1.2.2: jsprim@^1.2.2:
version "1.4.0" version "1.4.0"
resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.0.tgz#a3b87e40298d8c380552d8cc7628a0bb95a22918" resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.0.tgz#a3b87e40298d8c380552d8cc7628a0bb95a22918"
@ -1076,23 +1141,6 @@ jsprim@^1.2.2:
json-schema "0.2.3" json-schema "0.2.3"
verror "1.3.6" verror "1.3.6"
jwa@^1.1.4:
version "1.1.5"
resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.1.5.tgz#a0552ce0220742cd52e153774a32905c30e756e5"
dependencies:
base64url "2.0.0"
buffer-equal-constant-time "1.0.1"
ecdsa-sig-formatter "1.0.9"
safe-buffer "^5.0.1"
jws@^3.1.4:
version "3.1.4"
resolved "https://registry.yarnpkg.com/jws/-/jws-3.1.4.tgz#f9e8b9338e8a847277d6444b1464f61880e050a2"
dependencies:
base64url "^2.0.0"
jwa "^1.1.4"
safe-buffer "^5.0.1"
kind-of@^3.0.2: kind-of@^3.0.2:
version "3.2.2" version "3.2.2"
resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64" resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64"
@ -1157,10 +1205,6 @@ lodash.defaults@^3.1.2:
lodash.assign "^3.0.0" lodash.assign "^3.0.0"
lodash.restparam "^3.0.0" lodash.restparam "^3.0.0"
lodash.includes@^4.3.0:
version "4.3.0"
resolved "https://registry.yarnpkg.com/lodash.includes/-/lodash.includes-4.3.0.tgz#60bb98a87cb923c68ca1e51325483314849f553f"
lodash.isarguments@^3.0.0: lodash.isarguments@^3.0.0:
version "3.1.0" version "3.1.0"
resolved "https://registry.yarnpkg.com/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz#2f573d85c6a24289ff00663b491c1d338ff3458a" resolved "https://registry.yarnpkg.com/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz#2f573d85c6a24289ff00663b491c1d338ff3458a"
@ -1169,26 +1213,6 @@ lodash.isarray@^3.0.0:
version "3.0.4" version "3.0.4"
resolved "https://registry.yarnpkg.com/lodash.isarray/-/lodash.isarray-3.0.4.tgz#79e4eb88c36a8122af86f844aa9bcd851b5fbb55" resolved "https://registry.yarnpkg.com/lodash.isarray/-/lodash.isarray-3.0.4.tgz#79e4eb88c36a8122af86f844aa9bcd851b5fbb55"
lodash.isboolean@^3.0.3:
version "3.0.3"
resolved "https://registry.yarnpkg.com/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz#6c2e171db2a257cd96802fd43b01b20d5f5870f6"
lodash.isinteger@^4.0.4:
version "4.0.4"
resolved "https://registry.yarnpkg.com/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz#619c0af3d03f8b04c31f5882840b77b11cd68343"
lodash.isnumber@^3.0.3:
version "3.0.3"
resolved "https://registry.yarnpkg.com/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz#3ce76810c5928d03352301ac287317f11c0b1ffc"
lodash.isplainobject@^4.0.6:
version "4.0.6"
resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb"
lodash.isstring@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451"
lodash.keys@^3.0.0: lodash.keys@^3.0.0:
version "3.1.2" version "3.1.2"
resolved "https://registry.yarnpkg.com/lodash.keys/-/lodash.keys-3.1.2.tgz#4dbc0472b156be50a0b286855d1bd0b0c656098a" resolved "https://registry.yarnpkg.com/lodash.keys/-/lodash.keys-3.1.2.tgz#4dbc0472b156be50a0b286855d1bd0b0c656098a"
@ -1197,18 +1221,25 @@ lodash.keys@^3.0.0:
lodash.isarguments "^3.0.0" lodash.isarguments "^3.0.0"
lodash.isarray "^3.0.0" lodash.isarray "^3.0.0"
lodash.once@^4.0.0:
version "4.1.1"
resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac"
lodash.restparam@^3.0.0: lodash.restparam@^3.0.0:
version "3.6.1" version "3.6.1"
resolved "https://registry.yarnpkg.com/lodash.restparam/-/lodash.restparam-3.6.1.tgz#936a4e309ef330a7645ed4145986c85ae5b20805" resolved "https://registry.yarnpkg.com/lodash.restparam/-/lodash.restparam-3.6.1.tgz#936a4e309ef330a7645ed4145986c85ae5b20805"
lodash@^4.17.5:
version "4.17.5"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.5.tgz#99a92d65c0272debe8c96b6057bc8fbfa3bed511"
lodash@^4.5.1: lodash@^4.5.1:
version "4.17.4" version "4.17.4"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae"
log-ok@^0.1.1:
version "0.1.1"
resolved "https://registry.yarnpkg.com/log-ok/-/log-ok-0.1.1.tgz#bea3dd36acd0b8a7240d78736b5b97c65444a334"
dependencies:
ansi-green "^0.1.1"
success-symbol "^0.1.0"
lowercase-keys@^1.0.0: lowercase-keys@^1.0.0:
version "1.0.0" version "1.0.0"
resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.0.tgz#4e3366b39e7f5457e35f1324bdf6f88d0bfc7306" resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.0.tgz#4e3366b39e7f5457e35f1324bdf6f88d0bfc7306"
@ -1288,13 +1319,13 @@ minimist@^1.2.0:
version "1.2.0" version "1.2.0"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284"
"mkdirp@>=0.5 0", mkdirp@^0.5.1: "mkdirp@>=0.5 0", mkdirp@^0.5.0, mkdirp@^0.5.1:
version "0.5.1" version "0.5.1"
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903"
dependencies: dependencies:
minimist "0.0.8" minimist "0.0.8"
ms@2.0.0, ms@^2.0.0: ms@2.0.0:
version "2.0.0" version "2.0.0"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
@ -1306,6 +1337,24 @@ negotiator@0.6.1:
version "0.6.1" version "0.6.1"
resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.1.tgz#2b327184e8992101177b28563fb5e7102acd0ca9" resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.1.tgz#2b327184e8992101177b28563fb5e7102acd0ca9"
nock@^9.2.5:
version "9.2.5"
resolved "https://registry.yarnpkg.com/nock/-/nock-9.2.5.tgz#c131fc8d3c4723f386be0269739638be84733f2f"
dependencies:
chai "^4.1.2"
debug "^3.1.0"
deep-equal "^1.0.0"
json-stringify-safe "^5.0.1"
lodash "^4.17.5"
mkdirp "^0.5.0"
propagate "^1.0.0"
qs "^6.5.1"
semver "^5.5.0"
node-fetch@^2.1.2:
version "2.1.2"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.1.2.tgz#ab884e8e7e57e38a944753cec706f788d1768bb5"
node-pre-gyp@^0.6.36: node-pre-gyp@^0.6.36:
version "0.6.36" version "0.6.36"
resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.6.36.tgz#db604112cb74e0d477554e9b505b17abddfab786" resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.6.36.tgz#db604112cb74e0d477554e9b505b17abddfab786"
@ -1394,7 +1443,7 @@ on-finished@~2.3.0:
dependencies: dependencies:
ee-first "1.1.1" ee-first "1.1.1"
once@^1.3.0, once@^1.3.3: once@^1.3.0, once@^1.3.3, once@^1.4.0:
version "1.4.0" version "1.4.0"
resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
dependencies: dependencies:
@ -1457,6 +1506,10 @@ path-to-regexp@0.1.7:
version "0.1.7" version "0.1.7"
resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c"
pathval@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/pathval/-/pathval-1.1.0.tgz#b942e6d4bde653005ef6b71361def8727d0645e0"
pause-stream@0.0.11: pause-stream@0.0.11:
version "0.0.11" version "0.0.11"
resolved "https://registry.yarnpkg.com/pause-stream/-/pause-stream-0.0.11.tgz#fe5a34b0cbce12b5aa6a2b403ee2e73b602f1445" resolved "https://registry.yarnpkg.com/pause-stream/-/pause-stream-0.0.11.tgz#fe5a34b0cbce12b5aa6a2b403ee2e73b602f1445"
@ -1483,6 +1536,14 @@ process-nextick-args@~1.0.6:
version "1.0.7" version "1.0.7"
resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-1.0.7.tgz#150e20b756590ad3f91093f25a4f2ad8bff30ba3" resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-1.0.7.tgz#150e20b756590ad3f91093f25a4f2ad8bff30ba3"
process-nextick-args@~2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.0.tgz#a37d732f4271b4ab1ad070d35508e8290788ffaa"
propagate@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/propagate/-/propagate-1.0.0.tgz#00c2daeedda20e87e3782b344adba1cddd6ad709"
proxy-addr@~1.1.5: proxy-addr@~1.1.5:
version "1.1.5" version "1.1.5"
resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-1.1.5.tgz#71c0ee3b102de3f202f3b64f608d173fcba1a918" resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-1.1.5.tgz#71c0ee3b102de3f202f3b64f608d173fcba1a918"
@ -1508,7 +1569,7 @@ qs@6.5.0:
version "6.5.0" version "6.5.0"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.0.tgz#8d04954d364def3efc55b5a0793e1e2c8b1e6e49" resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.0.tgz#8d04954d364def3efc55b5a0793e1e2c8b1e6e49"
qs@6.5.1: qs@6.5.1, qs@^6.5.1:
version "6.5.1" version "6.5.1"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.1.tgz#349cdf6eef89ec45c12d7d5eb3fc0c870343a6d8" resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.1.tgz#349cdf6eef89ec45c12d7d5eb3fc0c870343a6d8"
@ -1545,6 +1606,18 @@ rc@^1.0.1, rc@^1.1.6, rc@^1.1.7:
minimist "^1.2.0" minimist "^1.2.0"
strip-json-comments "~2.0.1" strip-json-comments "~2.0.1"
readable-stream@^2.0.0, readable-stream@^2.3.5:
version "2.3.6"
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf"
dependencies:
core-util-is "~1.0.0"
inherits "~2.0.3"
isarray "~1.0.0"
process-nextick-args "~2.0.0"
safe-buffer "~5.1.1"
string_decoder "~1.1.1"
util-deprecate "~1.0.1"
readable-stream@^2.0.2, readable-stream@^2.0.5, readable-stream@^2.0.6, readable-stream@^2.1.4: readable-stream@^2.0.2, readable-stream@^2.0.5, readable-stream@^2.0.6, readable-stream@^2.1.4:
version "2.2.11" version "2.2.11"
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.2.11.tgz#0796b31f8d7688007ff0b93a8088d34aa17c0f72" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.2.11.tgz#0796b31f8d7688007ff0b93a8088d34aa17c0f72"
@ -1592,6 +1665,12 @@ registry-url@^3.0.3:
dependencies: dependencies:
rc "^1.0.1" rc "^1.0.1"
relative@^3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/relative/-/relative-3.0.2.tgz#0dcd8ec54a5d35a3c15e104503d65375b5a5367f"
dependencies:
isobject "^2.0.0"
remove-trailing-separator@^1.0.1: remove-trailing-separator@^1.0.1:
version "1.0.2" version "1.0.2"
resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.0.2.tgz#69b062d978727ad14dc6b56ba4ab772fd8d70511" resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.0.2.tgz#69b062d978727ad14dc6b56ba4ab772fd8d70511"
@ -1649,6 +1728,12 @@ rimraf@2, rimraf@^2.5.1, rimraf@^2.6.1:
dependencies: dependencies:
glob "^7.0.5" glob "^7.0.5"
rimraf@^2.6.2:
version "2.6.2"
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.2.tgz#2ed8150d24a16ea8651e6d6ef0f47c4158ce7a36"
dependencies:
glob "^7.0.5"
rx@2.3.24: rx@2.3.24:
version "2.3.24" version "2.3.24"
resolved "https://registry.yarnpkg.com/rx/-/rx-2.3.24.tgz#14f950a4217d7e35daa71bbcbe58eff68ea4b2b7" resolved "https://registry.yarnpkg.com/rx/-/rx-2.3.24.tgz#14f950a4217d7e35daa71bbcbe58eff68ea4b2b7"
@ -1657,6 +1742,10 @@ safe-buffer@^5.0.1:
version "5.1.0" version "5.1.0"
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.0.tgz#fe4c8460397f9eaaaa58e73be46273408a45e223" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.0.tgz#fe4c8460397f9eaaaa58e73be46273408a45e223"
safe-buffer@^5.1.1, safe-buffer@~5.1.0, safe-buffer@~5.1.1:
version "5.1.2"
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
safe-buffer@~5.0.1: safe-buffer@~5.0.1:
version "5.0.1" version "5.0.1"
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.0.1.tgz#d263ca54696cd8a306b5ca6551e92de57918fbe7" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.0.1.tgz#d263ca54696cd8a306b5ca6551e92de57918fbe7"
@ -1671,6 +1760,10 @@ semver@^5.0.3, semver@^5.1.0, semver@^5.3.0:
version "5.3.0" version "5.3.0"
resolved "https://registry.yarnpkg.com/semver/-/semver-5.3.0.tgz#9b2ce5d3de02d17c6012ad326aa6b4d0cf54f94f" resolved "https://registry.yarnpkg.com/semver/-/semver-5.3.0.tgz#9b2ce5d3de02d17c6012ad326aa6b4d0cf54f94f"
semver@^5.5.0:
version "5.5.0"
resolved "https://registry.yarnpkg.com/semver/-/semver-5.5.0.tgz#dc4bbc7a6ca9d916dee5d43516f0092b58f7b8ab"
send@0.15.4: send@0.15.4:
version "0.15.4" version "0.15.4"
resolved "https://registry.yarnpkg.com/send/-/send-0.15.4.tgz#985faa3e284b0273c793364a35c6737bd93905b9" resolved "https://registry.yarnpkg.com/send/-/send-0.15.4.tgz#985faa3e284b0273c793364a35c6737bd93905b9"
@ -1710,9 +1803,9 @@ setprototypeof@1.0.3:
version "1.0.3" version "1.0.3"
resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.0.3.tgz#66567e37043eeb4f04d91bd658c0cbefb55b8e04" resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.0.3.tgz#66567e37043eeb4f04d91bd658c0cbefb55b8e04"
shelljs@^0.7.8: shelljs@^0.8.1:
version "0.7.8" version "0.8.1"
resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.7.8.tgz#decbcf874b0d1e5fb72e14b164a9683048e9acb3" resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.8.1.tgz#729e038c413a2254c4078b95ed46e0397154a9f1"
dependencies: dependencies:
glob "^7.0.0" glob "^7.0.0"
interpret "^1.0.0" interpret "^1.0.0"
@ -1787,6 +1880,12 @@ string_decoder@~1.0.0:
dependencies: dependencies:
safe-buffer "~5.0.1" safe-buffer "~5.0.1"
string_decoder@~1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8"
dependencies:
safe-buffer "~5.1.0"
stringstream@~0.0.4: stringstream@~0.0.4:
version "0.0.5" version "0.0.5"
resolved "https://registry.yarnpkg.com/stringstream/-/stringstream-0.0.5.tgz#4e484cd4de5a0bbbee18e46307710a8a81621878" resolved "https://registry.yarnpkg.com/stringstream/-/stringstream-0.0.5.tgz#4e484cd4de5a0bbbee18e46307710a8a81621878"
@ -1811,6 +1910,10 @@ strip-json-comments@~2.0.1:
version "2.0.1" version "2.0.1"
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a"
success-symbol@^0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/success-symbol/-/success-symbol-0.1.0.tgz#24022e486f3bf1cdca094283b769c472d3b72897"
superagent@^3.0.0: superagent@^3.0.0:
version "3.5.2" version "3.5.2"
resolved "https://registry.yarnpkg.com/superagent/-/superagent-3.5.2.tgz#3361a3971567504c351063abeaae0faa23dbf3f8" resolved "https://registry.yarnpkg.com/superagent/-/superagent-3.5.2.tgz#3361a3971567504c351063abeaae0faa23dbf3f8"
@ -1860,6 +1963,18 @@ tar-pack@^3.4.0:
tar "^2.2.1" tar "^2.2.1"
uid-number "^0.0.6" uid-number "^0.0.6"
tar-stream@^1.6.0:
version "1.6.0"
resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-1.6.0.tgz#a50efaa7b17760b82c27b3cae4a301a8254a5715"
dependencies:
bl "^1.0.0"
buffer-alloc "^1.1.0"
end-of-stream "^1.0.0"
fs-constants "^1.0.0"
readable-stream "^2.0.0"
to-buffer "^1.1.0"
xtend "^4.0.0"
tar@^2.2.1: tar@^2.2.1:
version "2.2.1" version "2.2.1"
resolved "https://registry.yarnpkg.com/tar/-/tar-2.2.1.tgz#8e4d2a256c0e2185c6b18ad694aec968b83cb1d1" resolved "https://registry.yarnpkg.com/tar/-/tar-2.2.1.tgz#8e4d2a256c0e2185c6b18ad694aec968b83cb1d1"
@ -1882,6 +1997,10 @@ timed-out@^4.0.0:
version "4.0.1" version "4.0.1"
resolved "https://registry.yarnpkg.com/timed-out/-/timed-out-4.0.1.tgz#f32eacac5a175bea25d7fab565ab3ed8741ef56f" resolved "https://registry.yarnpkg.com/timed-out/-/timed-out-4.0.1.tgz#f32eacac5a175bea25d7fab565ab3ed8741ef56f"
to-buffer@^1.1.0:
version "1.1.1"
resolved "https://registry.yarnpkg.com/to-buffer/-/to-buffer-1.1.1.tgz#493bd48f62d7c43fcded313a03dcadb2e1213a80"
touch@^3.1.0: touch@^3.1.0:
version "3.1.0" version "3.1.0"
resolved "https://registry.yarnpkg.com/touch/-/touch-3.1.0.tgz#fe365f5f75ec9ed4e56825e0bb76d24ab74af83b" resolved "https://registry.yarnpkg.com/touch/-/touch-3.1.0.tgz#fe365f5f75ec9ed4e56825e0bb76d24ab74af83b"
@ -1939,6 +2058,10 @@ tweetnacl@^0.14.3, tweetnacl@~0.14.0:
version "0.14.5" version "0.14.5"
resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64"
type-detect@^4.0.0:
version "4.0.8"
resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c"
type-is@~1.6.15: type-is@~1.6.15:
version "1.6.15" version "1.6.15"
resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.15.tgz#cab10fb4909e441c82842eafe1ad646c81804410" resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.15.tgz#cab10fb4909e441c82842eafe1ad646c81804410"
@ -2047,7 +2170,7 @@ xdg-basedir@^3.0.0:
version "3.0.0" version "3.0.0"
resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-3.0.0.tgz#496b2cc109eca8dbacfe2dc72b603c17c5870ad4" resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-3.0.0.tgz#496b2cc109eca8dbacfe2dc72b603c17c5870ad4"
xtend@^4.0.1: xtend@^4.0.0:
version "4.0.1" version "4.0.1"
resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af" resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af"

View File

View File

@ -0,0 +1,12 @@
# 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

View File

@ -30,7 +30,7 @@ done
# Check servers # Check servers
origins=( origins=(
http://$AIO_UPLOAD_HOSTNAME:$AIO_UPLOAD_PORT http://$AIO_PREVIEW_SERVER_HOSTNAME:$AIO_PREVIEW_SERVER_PORT
http://$AIO_NGINX_HOSTNAME:$AIO_NGINX_PORT_HTTP http://$AIO_NGINX_HOSTNAME:$AIO_NGINX_PORT_HTTP
https://$AIO_NGINX_HOSTNAME:$AIO_NGINX_PORT_HTTPS https://$AIO_NGINX_HOSTNAME:$AIO_NGINX_PORT_HTTPS
) )

2
aio/aio-builds-setup/dockerbuild/scripts-sh/init.sh Executable file → Normal file
View File

@ -14,5 +14,5 @@ service cron start
service dnsmasq start service dnsmasq start
service nginx start service nginx start
service pm2-root start service pm2-root start
aio-upload-server-prod start aio-preview-server-prod start
echo [`date`] - Services started successfully. echo [`date`] - Services started successfully.

View File

@ -0,0 +1,14 @@
#!/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}

View File

@ -0,0 +1,16 @@
#!/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

View File

@ -0,0 +1,19 @@
# 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

View File

@ -1,14 +0,0 @@
#!/bin/bash
set -eu -o pipefail
# Set up env variables for production
export AIO_GITHUB_TOKEN=$(head -c -1 /aio-secrets/GITHUB_TOKEN 2>/dev/null || echo "MISSING_GITHUB_TOKEN")
export AIO_PREVIEW_DEPLOYMENT_TOKEN=$(head -c -1 /aio-secrets/PREVIEW_DEPLOYMENT_TOKEN 2>/dev/null || echo "MISSING_PREVIEW_DEPLOYMENT_TOKEN")
# Start the upload-server instance
action=$([ "$1" == "stop" ] && echo "stop" || echo "start")
pm2 $action $AIO_SCRIPTS_JS_DIR/dist/lib/upload-server \
--uid $AIO_WWW_USER \
--log /var/log/aio/upload-server-prod.log \
--name aio-upload-server-prod \
${@:2}

View File

@ -1,28 +0,0 @@
#!/bin/bash
set -eu -o pipefail
# Set up env variables for testing
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_TEAM_SLUGS=$TEST_AIO_GITHUB_TEAM_SLUGS
export AIO_REPO_SLUG=$TEST_AIO_REPO_SLUG
export AIO_TRUSTED_PR_LABEL=$TEST_AIO_TRUSTED_PR_LABEL
export AIO_UPLOAD_HOSTNAME=$TEST_AIO_UPLOAD_HOSTNAME
export AIO_UPLOAD_PORT=$TEST_AIO_UPLOAD_PORT
export AIO_GITHUB_TOKEN=$(head -c -1 /aio-secrets/TEST_GITHUB_TOKEN 2>/dev/null || echo "TEST_GITHUB_TOKEN")
export AIO_PREVIEW_DEPLOYMENT_TOKEN=$(head -c -1 /aio-secrets/TEST_PREVIEW_DEPLOYMENT_TOKEN 2>/dev/null || echo "TEST_PREVIEW_DEPLOYMENT_TOKEN")
# Start the upload-server instance
appName=aio-upload-server-test
if [[ "$1" == "stop" ]]; then
pm2 delete $appName
else
pm2 start $AIO_SCRIPTS_JS_DIR/dist/lib/verify-setup/start-test-upload-server.js \
--uid $AIO_WWW_USER \
--log /var/log/aio/upload-server-test.log \
--name $appName \
--no-autorestart \
${@:2}
fi

View File

@ -0,0 +1,2 @@
aio-verify-setup
ls -t /var/log/aio/preview-server-verify* | head -1 | xargs cat

View File

@ -2,7 +2,7 @@
set -eu -o pipefail set -eu -o pipefail
logFile=/var/log/aio/verify-setup.log logFile=/var/log/aio/verify-setup.log
uploadServerLogFile=/var/log/aio/upload-server-verify-setup.log previewServerLogFile=/var/log/aio/preview-server-verify-setup.log
exec 3>&1 exec 3>&1
exec >> $logFile exec >> $logFile
@ -23,18 +23,22 @@ function countdown {
} }
function onExit { function onExit {
aio-upload-server-test stop echo -e "Stopping Test Server"
aio-preview-server-test stop
echo -e "Full logs in '$logFile'.\n" > /dev/fd/3 echo -e "Full logs in '$logFile'.\n" > /dev/fd/3
} }
# Setup EXIT trap # Setup EXIT trap
trap 'onExit' EXIT trap 'onExit' EXIT
# Start an upload-server instance for testing # Start an preview-server instance for testing
aio-upload-server-test start --log $uploadServerLogFile echo -e "Starting Test Server"
aio-preview-server-test start --log $previewServerLogFile
# Give the upload-server some time to start :( # Give the preview-server some time to start :(
countdown "Starting" 5 > /dev/fd/3 countdown "Starting" 5 > /dev/fd/3
# Run the tests # Run the tests
echo Running the tests
source aio-test-env
node $AIO_SCRIPTS_JS_DIR/dist/lib/verify-setup | tee /dev/fd/3 node $AIO_SCRIPTS_JS_DIR/dist/lib/verify-setup | tee /dev/fd/3

View File

@ -10,18 +10,26 @@ environment variables and their default values can be found in the
Each variable has a `TEST_` prefixed counterpart, which is used for testing purposes. In most cases Each variable has a `TEST_` prefixed counterpart, which is used for testing purposes. In most cases
you don't need to specify values for those. you don't need to specify values for those.
- `AIO_ARTIFACT_PATH`:
The path used to identify the AIO build artifact on the CircleCI servers. This should be equal to
the path given in the `.circleci/config.yml` file for the
`aio_preview->steps->store_artifacts->destination` key.
- `AIO_BUILDS_DIR`: - `AIO_BUILDS_DIR`:
The directory (inside the container) where the uploaded build artifacts are kept. The directory (inside the container) where the hosted build artifacts are kept.
- `AIO_DOMAIN_NAME`: - `AIO_DOMAIN_NAME`:
The domain name of the server. The domain name of the server.
- `AIO_GITHUB_ORGANIZATION`: - `AIO_GITHUB_ORGANIZATION`:
The GitHub organization whose teams are whitelisted for accepting uploads. The GitHub organization whose teams are whitelisted for accepting build artifacts.
See also `AIO_GITHUB_TEAM_SLUGS`. See also `AIO_GITHUB_TEAM_SLUGS`.
- `AIO_GITHUB_REPO`:
The Github repository for which PRs will be hosted.
- `AIO_GITHUB_TEAM_SLUGS`: - `AIO_GITHUB_TEAM_SLUGS`:
A comma-separated list of teams, whose authors are allowed to upload PRs. A comma-separated list of teams, whose authors are allowed to preview PRs.
See also `AIO_GITHUB_ORGANIZATION`. See also `AIO_GITHUB_ORGANIZATION`.
- `AIO_NGINX_HOSTNAME`: - `AIO_NGINX_HOSTNAME`:
@ -36,22 +44,24 @@ you don't need to specify values for those.
The port number on which nginx listens for HTTPS connections. This should be mapped to the The port number on which nginx listens for HTTPS connections. This should be mapped to the
corresponding port on the host VM (as described [here](vm-setup--start-docker-container.md)). corresponding port on the host VM (as described [here](vm-setup--start-docker-container.md)).
- `AIO_REPO_SLUG`: - `AIO_SIGNIFICANT_FILES_PATTERN`:
The repository slug (in the form `<user>/<repo>`) for which PRs will be uploaded. The RegExp that determines whether a changed file indicates that a new preview needs to
be deployed. For example, if there is a changed file in the `/packages` directory then
some of the API docs might have changed, so we need to create a new preview.
- `AIO_TRUSTED_PR_LABEL`: - `AIO_TRUSTED_PR_LABEL`:
The PR whose presence indicates the PR has been manually verified and is allowed to have its The PR whose presence indicates the PR has been manually verified and is allowed to have its
build artifacts publicly served. This is useful for enabling previews for any PR (not only those build artifacts publicly served. This is useful for enabling previews for any PR (not only those
from trusted authors). from trusted authors).
- `AIO_UPLOAD_HOSTNAME`: - `AIO_PREVIEW_SERVER_HOSTNAME`:
The internal hostname for accessing the Node.js upload-server. This is used by nginx for The internal hostname for accessing the Node.js preview-server. This is used by nginx for
delegating upload requests and also for performing a periodic health-check. delegating web-hook requests and also for performing a periodic health-check.
- `AIO_UPLOAD_MAX_SIZE`: - `AIO_ARTIFACT_MAX_SIZE`:
The maximum allowed size for the uploaded gzip archive containing the build artifacts. Files The maximum allowed size for the gzip archive containing the build artifacts.
larger than this will be rejected. Files larger than this will be rejected.
- `AIO_UPLOAD_PORT`: - `AIO_PREVIEW_SERVER_PORT`:
The port number on which the Node.js upload-server listens for HTTP connections. This is used by The port number on which the Node.js preview-server listens for HTTP connections. This is used by
nginx for delegating upload requests and also for performing a periodic health-check. nginx for delegating web-hook requests and also for performing a periodic health-check.

View File

@ -5,8 +5,44 @@ TODO (gkalpak): Add docs. Mention:
- `aio-health-check` - `aio-health-check`
- `aio-verify-setup` - `aio-verify-setup`
- Test nginx accessible at: - Test nginx accessible at:
- `http://$TEST_AIO_NGINX_HOTNAME:$TEST_AIO_NGINX_PORT_HTTP` - `http://$TEST_AIO_NGINX_HOSTNAME:$TEST_AIO_NGINX_PORT_HTTP`
- `https://$TEST_AIO_NGINX_HOTNAME:$TEST_AIO_NGINX_PORT_HTTPS` - `https://$TEST_AIO_NGINX_HOSTNAME:$TEST_AIO_NGINX_PORT_HTTPS`
- Test upload-server accessible at: - Test preview-server accessible at:
- `http://$TEST_AIO_UPLOAD_HOTNAME:$TEST_AIO_UPLOAD_PORT` - `http://$TEST_AIO_PREVIEW_SERVER_HOSTNAME:$TEST_AIO_PREVIEW_SERVER_PORT`
- Local DNS (via dnsmasq) maps the above hostnames to 127.0.0.1 - Local DNS (via dnsmasq) maps the above hostnames to 127.0.0.1
## Developing the preview server TypeScript files
If you are running Docker on OS/X then you can benefit from linking the built TypeScript
files (i.e. `script-js/dist`) to the JavaScript files inside the Docker container.
First start watching and building the TypeScript files (in the host):
```bash
yarn build-watch
```
Now build, start and attach to the Docker container. See "Setting up the VM"
section in [TOC](_TOC.md). Then link the JavaScript folders (in the container):
```bash
aio-dev-mode
```
Now whenever you make changes to the TypeScript, it will be automatically built
in the host, and the changes are automatically available in the container.
You can then run the unit tests (in the container):
```bash
aio-verify-setup
```
Sometimes, the errors in the unit test log are not enough to tell you what went wrong.
In that case you can also look at the log of the preview-server itself.
A helper script that runs the unit tests (i.e. `aio-verify-setup`) and displays the
last relevant test-preview-server log is:
```bash
aio-verify-setup-and-log
```

View File

@ -2,10 +2,7 @@
TODO (gkalpak): Add docs. Mention: TODO (gkalpak): Add docs. Mention:
- Travis' JWT addon (+ limitations).
Relevant files: `.travis.yml`, `scripts/ci/env.sh`
- Testing on CI. - Testing on CI.
Relevant files: `scripts/ci/test-aio.sh`, `aio/aio-builds-setup/scripts/test.sh` Relevant files: `scripts/ci/test-aio.sh`, `aio/aio-builds-setup/scripts/test.sh`
- Deploying from CI. - Deploying from CI.
Relevant files: `scripts/ci/deploy.sh`, `aio/scripts/deploy-preview.sh`, Relevant files: `scripts/ci/deploy.sh`, `aio/scripts/deploy-to-firebase.sh`
`aio/scripts/deploy-to-firebase.sh`

View File

@ -2,9 +2,10 @@
## Objective ## Objective
Whenever a PR job is run on Travis, we want to build `angular.io` and upload the build artifacts to Whenever a PR job is run on the CI infrastructure (e.g. CircleCI), we want to build `angular.io`
a publicly accessible server so that collaborators (developers, designers, authors, etc) can preview and host the build artifacts on a publicly accessible server so that collaborators (developers,
the changes without having to checkout and build the app locally. designers, authors, etc) can preview the changes without having to checkout and build the app
locally.
## Source code ## Source code
@ -32,48 +33,38 @@ This section gives a brief summary of the several operations performed on CI and
container: container:
### On CI (Travis) ### On CI (CircleCI)
- Build job completes successfully. - Build job completes successfully.
- The CI script checks whether the build job was initiated by a PR against the angular/angular - The CI script checks whether the build job was initiated by a PR against the angular/angular
master branch. master branch.
- The CI script checks whether the PR has touched any files that might affect the angular.io app - The CI script checks whether the PR has touched any files that might affect the angular.io app
(currently the `aio/` or `packages/` directories, ignoring spec files). (currently the `aio/` or `packages/` directories, ignoring spec files).
- Optionally, the CI script can check whether the PR can be automatically verified (i.e. if the - The CI script gzips and stores the build artifacts in the CI infrastructure.
author of the PR is a member of one of the whitelisted GitHub teams or the PR has the specified - When the build completes CircleCI triggers a webhook on the preview-server.
"trusted PR" label).
**Note:**
For security reasons, the same checks will be performed on the server as well. This is an optional
step that can be used in case one wants to apply special logic depending on the outcome of the
pre-verification. For example:
1. One might want to deploy automatically verified PRs only. In that case, the pre-verification
helps avoid the wasted overhead associated with uploads that are going to be rejected (e.g.
building the artifacts, sending them to the server, running checks on the server, detecting the
reasons of deployment failure and whether to fail the build, etc).
2. One might want to apply additional logic (e.g. different tests) depending on whether the PR is
automatically verified or not).
- The CI script gzips and uploads the build artifacts to the server.
More info on how to set things up on CI can be found [here](misc--integrate-with-ci.md). More info on how to set things up on CI can be found [here](misc--integrate-with-ci.md).
### Uploading build artifacts ### Hosting build artifacts
- nginx receives the upload request.
- nginx checks that the uploaded gzip archive does not exceed the specified max file size, stores it - nginx receives the webhook trigger and passes it through to the preview server.
in a temporary location and passes the filepath to the Node.js upload-server. - The preview-server makes a request to CircleCI for the URL of the AIO build artifacts.
- The upload-server runs several checks to determine whether the request should be accepted and - The preview-server makes a request to this URL to receive the artifact - failing if the size
exceeds the specified max file size - and stores it in a temporary location.
- The preview-server runs several checks to determine whether the request should be accepted and
whether it should be publicly accessible or stored for later verification (more details can be whether it should be publicly accessible or stored for later verification (more details can be
found [here](overview--security-model.md)). found [here](overview--security-model.md)).
- The upload-server changes the "visibility" of the associated PR, if necessary. For example, if - The preview-server changes the "visibility" of the associated PR, if necessary. For example, if
builds for the same PR had been previously deployed as non-public and the current build has been builds for the same PR had been previously deployed as non-public and the current build has been
automatically verified, all previous builds are made public as well. automatically verified, all previous builds are made public as well.
If the PR transitions from "non-public" to "public", the upload-server posts a comment on the If the PR transitions from "non-public" to "public", the preview-server posts a comment on the
corresponding PR on GitHub mentioning the SHAs and the links where the previews can be found. corresponding PR on GitHub mentioning the SHAs and the links where the previews can be found.
- The upload-server verifies that the uploaded file is not trying to overwrite an existing build. - The preview-server verifies that it is not trying to overwrite an existing build.
- The upload-server deploys the artifacts to a sub-directory named after the PR number and the first - The preview-server deploys the artifacts to a sub-directory named after the PR number and the first
few characters of the SHA: `<PR>/<SHA>/` few characters of the SHA: `<PR>/<SHA>/`
(Non-publicly accessible PRs will be stored in a different location, but again derived from the PR (Non-publicly accessible PRs will be stored in a different location, but again derived from the PR
number and SHA.) number and SHA.)
- If the PR is publicly accessible, the upload-server posts a comment on the corresponding PR on - If the PR is publicly accessible, the preview-server posts a comment on the corresponding PR on
GitHub mentioning the SHA and the link where the preview can be found. GitHub mentioning the SHA and the link where the preview can be found.
More info on the possible HTTP status codes and their meaning can be found More info on the possible HTTP status codes and their meaning can be found
@ -82,24 +73,24 @@ More info on the possible HTTP status codes and their meaning can be found
### Updating PR visibility ### Updating PR visibility
- nginx receives a natification that a PR has been updated and passes it through to the - nginx receives a natification that a PR has been updated and passes it through to the
upload-server. This could, for example, be sent by a GitHub webhook every time a PR's labels preview-server. This could, for example, be sent by a GitHub webhook every time a PR's labels
change. change.
E.g.: `ngbuilds.io/pr-updated` (payload: `{"number":<PR>,"action":"labeled"}`) E.g.: `ngbuilds.io/pr-updated` (payload: `{"number":<PR>,"action":"labeled"}`)
- The request contains the PR number (as `number`) and optionally the action that triggered the - The request contains the PR number (as `number`) and optionally the action that triggered the
request (as `action`) in the payload. request (as `action`) in the payload.
- The upload-server verifies the payload and determines whether the `action` (if specified) could - The preview-server verifies the payload and determines whether the `action` (if specified) could
have led to PR visibility changes. Only requests that omit the `action` field altogether or have led to PR visibility changes. Only requests that omit the `action` field altogether or
specify an action that can affect visibility are further processed. specify an action that can affect visibility are further processed.
(Currently, the only actions that are considered capable of affecting visibility are `labeled` and (Currently, the only actions that are considered capable of affecting visibility are `labeled` and
`unlabeled`.) `unlabeled`.)
- The upload-server re-checks and if necessary updates the PR's visibility. - The preview-server re-checks and if necessary updates the PR's visibility.
More info on the possible HTTP status codes and their meaning can be found More info on the possible HTTP status codes and their meaning can be found
[here](overview--http-status-codes.md). [here](overview--http-status-codes.md).
### Serving build artifacts ### Serving build artifacts
- nginx receives a request for an uploaded resource on a subdomain corresponding to the PR and SHA. - nginx receives a request for a hosted preview resource on a subdomain corresponding to the PR and SHA.
E.g.: `pr<PR>-<SHA>.ngbuilds.io/path/to/resource` E.g.: `pr<PR>-<SHA>.ngbuilds.io/path/to/resource`
- nginx maps the subdomain to the correct sub-directory and serves the resource. - nginx maps the subdomain to the correct sub-directory and serves the resource.
E.g.: `/<PR>/<SHA>/path/to/resource` E.g.: `/<PR>/<SHA>/path/to/resource`
@ -117,4 +108,4 @@ that do not correspond with an open PR.
### Health-check ### Health-check
The docker service runs a periodic health-check that verifies the running conditions of the The docker service runs a periodic health-check that verifies the running conditions of the
container. This includes verifying the status of specific system services, the responsiveness of container. This includes verifying the status of specific system services, the responsiveness of
nginx and the upload-server and internet connectivity. nginx and the preview-server and internet connectivity.

View File

@ -1,8 +1,8 @@
# Overview - HTTP Status Codes # Overview - HTTP Status Codes
This is a list of all the possible HTTP status codes returned by the nginx anf upload servers, along This is a list of all the possible HTTP status codes returned by the nginx and preview servers, along
with a bried explanation of what they mean: with a brief explanation of what they mean:
## `http://*.ngbuilds.io/*` ## `http://*.ngbuilds.io/*`
@ -25,7 +25,7 @@ with a bried explanation of what they mean:
File not found. File not found.
## `https://ngbuilds.io/create-build/<pr>/<sha>` ## `https://ngbuilds.io/circle-build`
- **201 (Created)**: - **201 (Created)**:
Build deployed successfully and is publicly available. Build deployed successfully and is publicly available.
@ -33,14 +33,14 @@ with a bried explanation of what they mean:
- **202 (Accepted)**: - **202 (Accepted)**:
Build not automatically verifiable. Stored for later deployment (after re-verification). Build not automatically verifiable. Stored for later deployment (after re-verification).
- **400 (Bad Request)**: - **204 (No Content)**:
No payload. Build was not successful, so no further action is being taken.
- **401 (Unauthorized)**: - **400 (Bad Request)**:
No `AUTHORIZATION` header. Invalid payload.
- **403 (Forbidden)**: - **403 (Forbidden)**:
Unable to verify build (e.g. invalid JWT token, or unable to talk to 3rd-party APIs, etc). Unable to talk to 3rd-party APIs.
- **405 (Method Not Allowed)**: - **405 (Method Not Allowed)**:
Request method other than POST. Request method other than POST.
@ -49,9 +49,6 @@ with a bried explanation of what they mean:
Request to overwrite existing (public or non-public) directory (e.g. deploy existing build or Request to overwrite existing (public or non-public) directory (e.g. deploy existing build or
change PR visibility when the destination directory does already exist). change PR visibility when the destination directory does already exist).
- **413 (Payload Too Large)**:
Payload larger than size specified in `AIO_UPLOAD_MAX_SIZE`.
## `https://ngbuilds.io/health-check` ## `https://ngbuilds.io/health-check`

View File

@ -21,7 +21,7 @@ available:
from a git repository. See [here](vm-setup--update-docker-container.md) for more info. from a git repository. See [here](vm-setup--update-docker-container.md) for more info.
## Commands ## Production Commands
The following commands are available globally from inside the docker container. They are either used The following commands are available globally from inside the docker container. They are either used
by the container to perform its various operations or can be used ad-hoc, mainly for testing by the container to perform its various operations or can be used ad-hoc, mainly for testing
purposes. Each command is backed by a corresponding script inside purposes. Each command is backed by a corresponding script inside
@ -40,14 +40,27 @@ purposes. Each command is backed by a corresponding script inside
Initializes the container (mainly by starting the necessary services). Initializes the container (mainly by starting the necessary services).
_It is run (by default) when starting the container._ _It is run (by default) when starting the container._
- `aio-upload-server-prod`: - `aio-preview-server-prod`:
Spins up a Node.js upload-server instance. Spins up a Node.js preview-server instance.
_It is used in `aio-init` (see above) during initialization._ _It is used in `aio-init` (see above) during initialization._
- `aio-upload-server-test`:
Spins up a Node.js upload-server instance for tests. ## Developer Commands
- `aio-preview-server-test`:
Spins up a Node.js preview-server instance for tests.
_It is used in `aio-verify-setup` (see below) for running tests._ _It is used in `aio-verify-setup` (see below) for running tests._
- `aio-verify-setup`: - `aio-verify-setup`:
Runs a suite of e2e-like tests, mainly verifying the correct (inter)operation of nginx and the Runs a suite of e2e-like tests, mainly verifying the correct (inter)operation of nginx and the
Node.js upload-server. Node.js preview-server.
- `aio-verify-setup-and-log`:
Runs the `aio-verify-setup` command but also then dumps the logs from the preview server, which
gives additional useful debugging information. See the [debugging docs](misc--debug-docker-container.md)
for more info.
- `aio-dev-mode`:
Links external source files (from the Docker host) to interal source files (in the Docker
container). This makes it easier to use an IDE to edit files in the host that are then
tested in the container. See the [debugging docs](misc--debug-docker-container.md) for more info.

View File

@ -1,27 +1,27 @@
# Overview - Security model # Overview - Security model
Whenever a PR job is run on Travis, we want to build `angular.io` and upload the build artifacts to Whenever a PR job is run on CircleCI, we want to build `angular.io` and host the build artifacts on
a publicly accessible server so that collaborators (developers, designers, authors, etc) can preview a publicly accessible server so that collaborators (developers, designers, authors, etc) can preview
the changes without having to checkout and build the app locally. the changes without having to checkout and build the app locally.
This document discusses the security considerations associated with uploading build artifacts as This document discusses the security considerations associated with moving build artifacts as
part of the CI setup and serving them publicly. part of the CI process and serving them publicly.
## Security objectives ## Security objectives
- **Prevent uploading arbitrary content to our servers.** - **Prevent hosting arbitrary content to on servers.**
Since there is no restriction on who can submit a PR, we cannot allow any PR's build artifacts to Since there is no restriction on who can submit a PR, we cannot allow arbitrary untrusted PRs'
be uploaded. build artifacts to be hosted.
- **Prevent overwriting other peoples uploaded content.** - **Prevent overwriting other people's hosted build artifacts.**
There needs to be a mechanism in place to ensure that the uploaded content does indeed correspond There needs to be a mechanism in place to ensure that the hosted content does indeed correspond
to the PR indicated by its URL. to the PR indicated by its URL.
- **Prevent arbitrary access on the server.** - **Prevent arbitrary access on the server.**
Since the PR author has full access over the build artifacts that would be uploaded, we must Since the PR author has full access over the build artifacts that would be hosted, we must
ensure that the uploaded files will not enable arbitrary access to the server or expose sensitive ensure that the build artifacts will not have arbitrary access to the server or expose sensitive
info. info.
@ -30,7 +30,7 @@ part of the CI setup and serving them publicly.
- Because the PR author can change the scripts run on CI, any security mechanisms must be immune to - Because the PR author can change the scripts run on CI, any security mechanisms must be immune to
such changes. such changes.
- For security reasons, encrypted Travis variables are not available to PRs, so we can't rely on - For security reasons, encrypted CircleCI variables are not available to PRs, so we can't rely on
them to implement security. them to implement security.
@ -40,35 +40,42 @@ part of the CI setup and serving them publicly.
### In a nutshell ### In a nutshell
The implemented approach can be broken up to the following sub-tasks: The implemented approach can be broken up to the following sub-tasks:
1. Verify which PR the uploaded artifacts correspond to. 0. Receive notification from CircleCI of a completed build.
1. Verify that the build is valid and download the artifact.
2. Fetch the PR's metadata, including author and labels. 2. Fetch the PR's metadata, including author and labels.
3. Check whether the PR can be automatically verified as "trusted" (based on its author or labels). 3. Check whether the PR can be automatically verified as "trusted" (based on its author or labels).
4. If necessary, update the corresponding PR's verification status. 4. If necessary, update the corresponding PR's verification status.
5. Deploy the artifacts to the corresponding PR's directory. 5. Deploy the artifacts to the corresponding PR's directory.
6. Prevent overwriting previously deployed artifacts (which ensures that the guarantees established 6. Prevent overwriting previously deployed artifacts (which ensures that the guarantees established
during deployment will remain valid until the artifacts are removed). during deployment will remain valid until the artifacts are removed).
7. Prevent uploaded files from accessing anything outside their directory. 7. Prevent hosted preview files from accessing anything outside their directory.
### Implementation details ### Implementation details
This section describes how each of the aforementioned sub-tasks is accomplished: This section describes how each of the aforementioned sub-tasks is accomplished:
1. **Verify which PR the uploaded artifacts correspond to.** 0. **Receive notification from CircleCI of a completed build**
We are taking advantage of Travis' [JWT addon](https://docs.travis-ci.com/user/jwt). By sharing CircleCI is configured to trigger a webhook on our preview-server whenever a build completes.
a secret between Travis (which keeps it private but uses it to sign a JWT) and the server (which The payload contains the number of the build that completed.
uses it to verify the authenticity of the JWT), we can accomplish the following:
a. Verify that the upload request comes from Travis.
b. Determine the PR that these artifacts correspond to (since Travis puts that information into
the JWT, without the PR author being able to modify it).
_Note:_ 1. **Verify that the build is valid and download the artifact.**
_There are currently certain limitation in the implementation of the JWT addon._
_See the next section for more details._ We cannot trust that the data in the webhook trigger is authentic, so we only extract the build
number and then run a direct query against the CircleCI API to get hold of the real data for
the given build number.
If the build was not successful then we ignore this trigger. Otherwise we check that the
associated github organisation and repository are what we expect (e.g. angular/angular).
Next we make another call to the CircleCI API to get a list of the URLS for artifacts of that
build. If there is one that matches the configured artifact path, we download the contents of the
build artifact and store it in a local folder. This download has a maximum size limit to prevent
PRs from producing artifacts that are so large they would cause the preview server to crash.
2. **Fetch the PR's metadata, including author and labels**. 2. **Fetch the PR's metadata, including author and labels**.
Once we have securely associated the uploaded artifacts to a PR, we retrieve the PR's metadata - Once we have securely downloaded the artifact for a build, we retrieve the PR's metadata -
including the author's username and the labels - using the including the author's username and the labels - using the
[GitHub API](https://developer.github.com/v3/). [GitHub API](https://developer.github.com/v3/).
To avoid rate-limit restrictions, we use a Personal Access Token (issued by To avoid rate-limit restrictions, we use a Personal Access Token (issued by
@ -91,48 +98,42 @@ This section describes how each of the aforementioned sub-tasks is accomplished:
Once we have determined whether the PR is considered "trusted", we update its "visibility" (i.e. Once we have determined whether the PR is considered "trusted", we update its "visibility" (i.e.
whether it is publicly accessible or not), based on the new verification status. For example, if whether it is publicly accessible or not), based on the new verification status. For example, if
a PR was initially considered "not trusted" but the check triggered by a new build determined a PR was initially considered "not trusted" but the check triggered by a new build determined
otherwise, the PR (and all the previously uploaded previews) are made public. It works the same otherwise, the PR (and all the previously hosted previews) are made public. It works the same
way if a PR has gone from "trusted" to "not trusted". way if a PR has gone from "trusted" to "not trusted".
5. **Deploy the artifacts to the corresponding PR's directory.** 5. **Deploy the artifacts to the corresponding PR's directory.**
With the preceding steps, we have verified that the uploaded artifacts have been uploaded by With the preceding steps, we have verified that the build artifacts are valid.
Travis. Additionally, we have determined whether the PR can be trusted to have its previews Additionally, we have determined whether the PR can be trusted to have its previews
publicly accessible or whether further verification is necessary. The artifacts will be stored to publicly accessible or whether further verification is necessary. The artifacts will be stored to
the PR's directory, but will not be publicly accessible unless the PR has been verified. the PR's directory, but will not be publicly accessible unless the PR has been verified.
Essentially, as long as sub-tasks 1, 2 and 3 can be securely accomplished, it is possible to Essentially, as long as sub-tasks 1, 2 and 3 can be securely accomplished, it is possible to
"project" the trust we have in a team's members through the PR and Travis to the build artifacts. "project" the trust we have in a team's members through the PR to the build artifacts.
6. **Prevent overwriting previously deployed artifacts**. 6. **Prevent overwriting previously deployed artifacts**.
In order to enforce this restriction (and ensure that the deployed artifacts' validity is In order to enforce this restriction (and ensure that the deployed artifacts' validity is
preserved throughout their "lifetime"), the server that handles the upload (currently a Node.js preserved throughout their "lifetime"), the server that handles the artifacts (currently a Node.js
Express server) rejects uploads that target an existing directory. Express server) rejects builds that have already been handled.
_Note: A PR can contain multiple uploads; one for each SHA that was built on Travis._ _Note: A PR can contain multiple builds; one for each SHA that was built on CircleCI._
7. **Prevent uploaded files from accessing anything outside their directory.** 7. **Prevent hosted preview files from accessing anything outside their directory.**
Nginx (which is used to serve the uploaded artifacts) has been configured to not follow symlinks Nginx (which is used to serve the hosted preview) has been configured to not follow symlinks
outside of the directory where the build artifacts are stored. outside of the directory where the preview files are stored.
## Assumptions / Things to keep in mind ## Assumptions / Things to keep in mind
- Each trusted PR author has full control over the content that is uploaded for their PRs. Part of - Other than the initial webhook trigger, which provides a build number, all requests for data come
the security model relies on the trustworthiness of these authors. from the preview-server making requests to well defined API endpoints (e.g. CircleCI and Github).
This means that any secret access keys need only be stored on the preview-server and not on any of
the CI build infrastructure (e.g. CircleCI).
- Adding the specified label on a PR and marking it as trusted, gives the author full control over - Each trusted PR author has full control over the content that is hosted as a preview for their PRs.
the content that is uploaded for the specific PR (e.g. by pushing more commits to it). The user Part of the security model relies on the trustworthiness of these authors.
adding the label is responsible for ensuring that this control is not abused and that the PR is
either closed (one way of another) or the access is revoked.
- If anyone gets access to the `PREVIEW_DEPLOYMENT_TOKEN` (a.k.a. `NGBUILDS_IO_KEY` on - Adding the specified label on a PR to mark it as trusted, gives the author full control over
angular/angular) variable generated for each Travis job, they will be able to impersonate the the content that is hosted for the specific PR preview (e.g. by pushing more commits to it).
corresponding PR's author on the preview server for as long as the token is valid (currently 90 The user adding the label is responsible for ensuring that this control is not abused and that
mins). Because of this, the value of the `PREVIEW_DEPLOYMENT_TOKEN` should not be made publicly the PR is either closed (one way of another) or the access is revoked.
accessible (e.g. by printing it on the Travis job log).
- Travis does only allow specific whitelisted property names to be used with the JWT addon. The only
known such property at the time is `SAUCE_ACCESS_KEY` (used for integration with SauceLabs). In
order to be able to actually use the JWT addon we had to name the encrypted variable
`SAUCE_ACCESS_KEY` (which we later re-assign to `NGBUILDS_IO_KEY`).

View File

@ -1,6 +1,12 @@
# VM setup - Create docker image # VM setup - Create docker image
## Install node and yarn
- Install [nvm](https://github.com/creationix/nvm#installation).
- Install node.js: `nvm install 8`
- Install yarn: `npm -g install yarn`
## Checkout repository ## Checkout repository
- `git clone <repo-url>` - `git clone <repo-url>`
@ -21,7 +27,7 @@ The following commands would create a docker image from GitHub repo `foo/bar` to
- `git clone https://github.com/foo/bar.git foobar` - `git clone https://github.com/foo/bar.git foobar`
- Run: - Run:
``` ```
./foobar/aio-builds-setup/scripts/build.sh foobar-builds \ ./foobar/aio-builds-setup/scripts/create-image.sh foobar-builds \
--build-arg AIO_REPO_SLUG=foo/bar \ --build-arg AIO_REPO_SLUG=foo/bar \
--build-arg AIO_DOMAIN_NAME=foobar-builds.io \ --build-arg AIO_DOMAIN_NAME=foobar-builds.io \
--build-arg AIO_GITHUB_ORGANIZATION=foo \ --build-arg AIO_GITHUB_ORGANIZATION=foo \

View File

@ -12,8 +12,8 @@ More info on how to create `secrets` directory and files can be found
## Create directory for build artifacts ## Create directory for build artifacts
The uploaded build artifacts should be kept on a directory outside the docker container, so it is The build artifacts should be kept on a directory outside the docker container, so it is
easier to replace the container without losing the uploaded builds. For portability across VMs a easier to replace the container without losing the builds. For portability across VMs a
persistent disk can be used (as described [here](vm-setup--attach-persistent-disk.md)). persistent disk can be used (as described [here](vm-setup--attach-persistent-disk.md)).
**Note:** The directories created inside that directory will be owned by user `www-data`. **Note:** The directories created inside that directory will be owned by user `www-data`.
@ -21,7 +21,7 @@ persistent disk can be used (as described [here](vm-setup--attach-persistent-dis
## Create SSL certificates (Optional for dev) ## Create SSL certificates (Optional for dev)
The host VM can attach a directory containing the SSL certificate and key to be used by the nginx The host VM can attach a directory containing the SSL certificate and key to be used by the nginx
server for serving the uploaded build artifacts. More info on how to attach the directory when server for serving the hosted previews. More info on how to attach the directory when
starting the container can be found [here](vm-setup--start-docker-container.md). starting the container can be found [here](vm-setup--start-docker-container.md).
In order for the container to be able to find the certificate and key, they should be named In order for the container to be able to find the certificate and key, they should be named
@ -61,15 +61,15 @@ The following log files are kept in this directory:
used when running tests locally from inside the container, e.g. with the `aio-verify-setup` used when running tests locally from inside the container, e.g. with the `aio-verify-setup`
command. (See [here](overview--scripts-and-commands.md) for more info.) command. (See [here](overview--scripts-and-commands.md) for more info.)
- `upload-server-{prod,test,verify-setup}-*.log`: - `preview-server-{prod,test,verify-setup}-*.log`:
The logs produced by the Node.js upload-server while serving either: The logs produced by the Node.js preview-server while serving either:
- `-prod`: "Production" files (g.g during normal operation). - `-prod`: "Production" files (g.g during normal operation).
- `-test`: "Test" files (e.g. when a test instance is started with the `aio-upload-server-test` - `-test`: "Test" files (e.g. when a test instance is started with the `aio-preview-server-test`
command). command).
- `-verify-setup`: "Test" files, but while running `aio-verify-setup`. - `-verify-setup`: "Test" files, but while running `aio-verify-setup`.
(See [here](overview--scripts-and-commands.md) for more info the commands mentioned above.) (See [here](overview--scripts-and-commands.md) for more info the commands mentioned above.)
- `verify-setup.log`: - `verify-setup.log`:
The output of the `aio-verify-setup` command (e.g. Jasmine output), except for upload-server The output of the `aio-verify-setup` command (e.g. Jasmine output), except for preview-server
output which is logged to `upload-server-verify-setup-*.log` (see above). output which is logged to `preview-server-verify-setup-*.log` (see above).

View File

@ -9,17 +9,13 @@ Necessary secrets:
- Used for: - Used for:
- Retrieving open PRs without rate-limiting. - Retrieving open PRs without rate-limiting.
- Retrieving PR author. - Retrieving PR author.
- Retrieving members of the `angular-core` team. - Retrieving members of the trusted GitHub teams.
- Posting comments with preview links on PRs. - Posting comments with preview links on PRs.
2. `PREVIEW_DEPLOYMENT_TOKEN` 2. `CIRCLE_CI_TOKEN`
- Used for: - Used for:
- Decoding the JWT tokens received with `/create-build` requests. - Retrieving build information.
- Downloading build artifacts.
**Note:**
`TEST_GITHUB_TOKEN` and `TEST_PREVIEW_DEPLOYMENT_TOKEN` can also be created similar to their
non-TEST counterparts and they will be loaded when running `aio-verify-setup`, but it is currently
not clear if/how they can be used in tests.
## Create secrets ## Create secrets
@ -28,25 +24,15 @@ not clear if/how they can be used in tests.
- Visit https://github.com/settings/tokens. - Visit https://github.com/settings/tokens.
- Generate new token with the `public_repo` scope. - Generate new token with the `public_repo` scope.
2. `PREVIEW_DEPLOYMENT_TOKEN` 2. `CIRCLE_CI_TOKEN`
- Just generate a hard-to-guess character sequence. - Visit https://circleci.com/gh/angular/angular/edit#api
- Add it to `.travis.yml` under `addons -> jwt -> secure`. - Create an API token with `Build Artifacts` scope
Can be added automatically with: `travis encrypt --add addons.jwt PREVIEW_DEPLOYMENT_TOKEN=<access-key>`
**Note:**
Due to [travis-ci/travis-ci#7223](https://github.com/travis-ci/travis-ci/issues/7223) it is not
currently possible to use the JWT addon (as described above) for anything other than the
`SAUCE_ACCESS_KEY` variable. You can get creative, though...
**WARNING**
TO avoid arbitrary uploads, make sure the `PREVIEW_DEPLOYMENT_TOKEN` is NOT printed in the Travis log.
## Save secrets on the VM ## Save secrets on the VM
- `sudo mkdir /aio-secrets` - `sudo mkdir /aio-secrets`
- `sudo touch /aio-secrets/GITHUB_TOKEN` - `sudo touch /aio-secrets/GITHUB_TOKEN`
- Insert `<github-token>` into `/aio-secrets/GITHUB_TOKEN`. - Insert `<github-token>` into `/aio-secrets/GITHUB_TOKEN`.
- `sudo touch /aio-secrets/PREVIEW_DEPLOYMENT_TOKEN` - `sudo touch /aio-secrets/CIRCLE_CI_TOKEN`
- Insert `<access-token>` into `/aio-secrets/PREVIEW_DEPLOYMENT_TOKEN`. - Insert `<access-token>` into `/aio-secrets/CIRCLE_CI_TOKEN`.
- `sudo chmod 400 /aio-secrets/*` - `sudo chmod 400 /aio-secrets/*`

View File

@ -13,14 +13,15 @@ sudo docker run \
--publish 80:80 \ --publish 80:80 \
--publish 443:443 \ --publish 443:443 \
--restart unless-stopped \ --restart unless-stopped \
[--volume <host-cert-dir>:/etc/ssl/localcerts:ro] \
--volume <host-secrets-dir>:/aio-secrets:ro \ --volume <host-secrets-dir>:/aio-secrets:ro \
--volume <host-builds-dir>:/var/www/aio-builds \ --volume <host-builds-dir>:/var/www/aio-builds \
[--volume <host-cert-dir>:/etc/ssl/localcerts:ro] \
[--volume <host-logs-dir>:/var/log/aio] \ [--volume <host-logs-dir>:/var/log/aio] \
[--volume <host-dockerbuild-dir>:/dockerbuild] \
<name>[:<tag>] <name>[:<tag>]
``` ```
Below is the same command with inline comments explaining each option. The aPI docs for `docker run` Below is the same command with inline comments explaining each option. The API docs for `docker run`
can be found [here](https://docs.docker.com/engine/reference/run/). can be found [here](https://docs.docker.com/engine/reference/run/).
``` ```
@ -30,7 +31,7 @@ sudo docker run \
--detach \ --detach \
# Use the local DNS server. # Use the local DNS server.
# (This is necessary for mapping internal URLs, e.g. for the Node.js upload-server.) # (This is necessary for mapping internal URLs, e.g. for the Node.js preview-server.)
--dns 127.0.0.1 \ --dns 127.0.0.1 \
# USe `<instance-name>` as an alias for the container. # USe `<instance-name>` as an alias for the container.
@ -45,28 +46,32 @@ sudo docker run \
# (This ensures that the container will be automatically started on boot.) # (This ensures that the container will be automatically started on boot.)
--restart unless-stopped \ --restart unless-stopped \
# The directory the contains the SSL certificates.
# (See [here](vm-setup--create-host-dirs-and-files.md) for more info.)
# If not provided, the container will use self-signed certificates.
[--volume <host-cert-dir>:/etc/ssl/localcerts:ro] \
# The directory the contains the secrets (e.g. GitHub token, JWT secret, etc). # The directory the contains the secrets (e.g. GitHub token, JWT secret, etc).
# (See [here](vm-setup--set-up-secrets.md) for more info.) # (See [here](vm-setup--set-up-secrets.md) for more info.)
--volume <host-secrets-dir>:/aio-secrets:ro \ --volume <host-secrets-dir>:/aio-secrets:ro \
# The uploaded build artifacts will stored to and served from this directory. # The build artifacts and hosted previews will stored to and served from this directory.
# (If you are using a persistent disk - as described [here](vm-setup--attach-persistent-disk.md) - # (If you are using a persistent disk - as described [here](vm-setup--attach-persistent-disk.md) -
# this will be a directory inside the disk.) # this will be a directory inside the disk.)
--volume <host-builds-dir>:/var/www/aio-builds \ --volume <host-builds-dir>:/var/www/aio-builds \
# The directory the contains the SSL certificates.
# (See [here](vm-setup--create-host-dirs-and-files.md) for more info.)
# If not provided, the container will use self-signed certificates.
[--volume <host-cert-dir>:/etc/ssl/localcerts:ro] \
# The directory where the logs are being kept. # The directory where the logs are being kept.
# (See [here](vm-setup--create-host-dirs-and-files.md) for more info.) # (See [here](vm-setup--create-host-dirs-and-files.md) for more info.)
# If not provided, the logs will be kept inside the container, which means they will be lost # If not provided, the logs will be kept inside the container, which means they will be lost
# whenever a new container is created. # whenever a new container is created.
[--volume <host-logs-dir>:/var/log/aio] \ [--volume <host-logs-dir>:/var/log/aio] \
# This directory allows you to share the source scripts between the host and the container when
# debugging. (See [here](misc--debug-docker-container.md) for how to set this up.)
[--volume <host-dockerbuild-dir>:/dockerbuild] \
# The name of the docker image to use (and an optional tag; defaults to `latest`). # The name of the docker image to use (and an optional tag; defaults to `latest`).
# (See [here](vm-setup--create-docker-image.md) for instructions on how to create the iamge.) # (See [here](vm-setup--create-docker-image.md) for instructions on how to create the image.)
<name>[:<tag>] <name>[:<tag>]
``` ```
@ -74,7 +79,8 @@ sudo docker run \
## Example ## Example
The following command would start a docker container based on the previously created `foobar-builds` The following command would start a docker container based on the previously created `foobar-builds`
docker image, alias it as 'foobar-builds-1' and map predefined directories on the host VM to be used docker image, alias it as 'foobar-builds-1' and map predefined directories on the host VM to be used
by the container for accesing secrets and SSL certificates and keeping the build artifacts and logs. by the container for accessing secrets and SSL certificates and keeping the build artifacts and logs;
and will map the source scripts from the host to the container.
``` ```
sudo docker run \ sudo docker run \
@ -84,9 +90,10 @@ sudo docker run \
--publish 80:80 \ --publish 80:80 \
--publish 443:443 \ --publish 443:443 \
--restart unless-stopped \ --restart unless-stopped \
--volume /etc/ssl/localcerts:/etc/ssl/localcerts:ro \
--volume /foobar-secrets:/aio-secrets:ro \ --volume /foobar-secrets:/aio-secrets:ro \
--volume /mnt/disks/foobar-builds:/var/www/aio-builds \ --volume /mnt/disks/foobar-builds:/var/www/aio-builds \
--volume /etc/ssl/localcerts:/etc/ssl/localcerts:ro \
--volume /foobar-logs:/var/log/aio \ --volume /foobar-logs:/var/log/aio \
--volume ~/angular/aio/aio-builds-setup/dockerbuild:/dockerbuild \
foobar-builds foobar-builds
``` ```

BIN
aio/content/examples/.DS_Store vendored Normal file

Binary file not shown.

View File

@ -76,8 +76,8 @@ aot-compiler/**/*.factory.d.ts
# universal # universal
!universal/webpack.server.config.js !universal/webpack.server.config.js
# plunkers # stackblitz
*plnkr.no-link.html *stackblitz.no-link.html
# ngUpgrade testing # ngUpgrade testing
!upgrade-phonecat-*/**/karma.conf.js !upgrade-phonecat-*/**/karma.conf.js

View File

@ -1,6 +1,5 @@
{ {
"description": "AngularJS to Angular Quick Reference", "description": "AngularJS to Angular Quick Reference",
"basePath": "src/",
"files":[ "files":[
"!**/*.d.ts", "!**/*.d.ts",
"!**/*.js", "!**/*.js",

View File

@ -1,6 +1,5 @@
{ {
"description": "Angular Animations", "description": "Angular Animations",
"basePath": "src/",
"files":[ "files":[
"!**/*.d.ts", "!**/*.d.ts",
"!**/*.js" "!**/*.js"

View File

@ -15,7 +15,7 @@ export class BackendService {
getAll(type: Type<any>): PromiseLike<any[]> { getAll(type: Type<any>): PromiseLike<any[]> {
if (type === Hero) { if (type === Hero) {
// TODO get from the database // TODO: get from the database
return Promise.resolve<Hero[]>(HEROES); return Promise.resolve<Hero[]>(HEROES);
} }
let err = new Error('Cannot get object of this type'); let err = new Error('Cannot get object of this type');

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