Compare commits
213 Commits
Author | SHA1 | Date | |
---|---|---|---|
b69f0faee6 | |||
9c4fcd370a | |||
8b8b4cb794 | |||
53378749ef | |||
3e61bf76c4 | |||
b4fab438ea | |||
426191bc4e | |||
6307581f54 | |||
591d894329 | |||
48772f189f | |||
70b1ff11d5 | |||
f1c9197e07 | |||
991f8ad687 | |||
efa45b9299 | |||
45ae14c461 | |||
5ea9b62d58 | |||
276357780c | |||
2108c23b42 | |||
d48b7d3a8e | |||
00c97417e3 | |||
16e6c6e7f9 | |||
f9c4c3710e | |||
eb90ba1321 | |||
9e626415eb | |||
f632ca4284 | |||
620c2161f6 | |||
30beb52673 | |||
fe09e10d02 | |||
ee7d134d8e | |||
2ab90578f3 | |||
8f7cce38f8 | |||
a3180937c1 | |||
3d52675c1b | |||
14afb9d314 | |||
62b973773f | |||
8bdc921502 | |||
cd99c51a40 | |||
3fb93fdb5e | |||
cf5752738a | |||
00de9ff531 | |||
12a2099265 | |||
cdc0c8ff58 | |||
14fbc2e7c0 | |||
ea8fe9316e | |||
b658afc3de | |||
706a48e9c1 | |||
d472aa201a | |||
6df57adab3 | |||
54df1e287d | |||
6d55a807cd | |||
670f2e5599 | |||
408bb89e8f | |||
ed45830044 | |||
37b200010b | |||
571adbe40e | |||
10bfa8a11e | |||
c460757d2e | |||
1a476b81ab | |||
27b87e893c | |||
7080fccb38 | |||
54f96bfe27 | |||
61e6618429 | |||
e1fea47400 | |||
f31b0d664d | |||
79b634692a | |||
6909171c0a | |||
ec4ae60fb8 | |||
7559b7898e | |||
7728d7efc6 | |||
2c1ef08466 | |||
5f1dcfc228 | |||
f9f2b23afc | |||
ad3661cff7 | |||
804173c8a6 | |||
a0c790e7bd | |||
7205d7a568 | |||
e57eec9b13 | |||
5e1b339f46 | |||
6b23e2f427 | |||
08dba3f93b | |||
2624fa5ae2 | |||
a1458d9cb9 | |||
ffbdf74e92 | |||
589826f5e5 | |||
6f5e6374f8 | |||
a309d62806 | |||
9699c02642 | |||
6040c66e95 | |||
347fec5e8c | |||
c7bbf0573a | |||
c2fda10a73 | |||
b38edeb055 | |||
2407beb17e | |||
b6418039e0 | |||
7dd7722220 | |||
9b14f62387 | |||
107655e43f | |||
162bddf0b8 | |||
81c18b7241 | |||
0c4b561999 | |||
563577eeab | |||
17e000a678 | |||
b8806a656f | |||
ee5cd52d5f | |||
84a72b774e | |||
9e28f58ec8 | |||
10e2db000c | |||
098d35ef64 | |||
d5a8f6ebfe | |||
9415e6603f | |||
927ea2462e | |||
1d6465b596 | |||
58012df46f | |||
21c5b177ef | |||
bc10291d42 | |||
9344c86242 | |||
6b5b3a7269 | |||
58c5e86209 | |||
1657fa756f | |||
2ff85c6790 | |||
353de5191c | |||
ea5ac89b50 | |||
f5ce60e6bc | |||
7a2903993a | |||
a29a0df183 | |||
746fed453a | |||
64e1b9cc9a | |||
0947f4a358 | |||
f10d5a5572 | |||
93f57d7d70 | |||
af1c4db0e4 | |||
4f43029570 | |||
8a09015211 | |||
f4b7bf7e38 | |||
d20313698d | |||
74954f1a8d | |||
368aed087c | |||
975341a77e | |||
c112232fa3 | |||
36348325de | |||
fdbe62f112 | |||
af9ba91e5c | |||
b840ec3684 | |||
b38552f36e | |||
572885dcc8 | |||
33906489ad | |||
59299de374 | |||
f99793700f | |||
77860a0023 | |||
93d834a0b2 | |||
63a5f33e99 | |||
20eb5cfd59 | |||
4ab7353a9f | |||
eb23460e09 | |||
2e8efdaffa | |||
3cfb13f746 | |||
7de1ae2be8 | |||
341b812a10 | |||
09512c7770 | |||
1ec0a53fec | |||
3607a48a08 | |||
62957fa515 | |||
ab81c1c068 | |||
25234227ec | |||
71a8ef5a15 | |||
4dd6deca16 | |||
f8db05ef7d | |||
623b9c6e58 | |||
8a547eeee0 | |||
4211432fc8 | |||
b8c39cdf71 | |||
9c7a84de51 | |||
dbc6a4cb12 | |||
784410e3c8 | |||
90a5a1ef43 | |||
301f99cd6c | |||
64e63b9422 | |||
b192dd5761 | |||
96aa3bb135 | |||
8abc1df2c1 | |||
f5eb528a5c | |||
5cf06a9f3f | |||
ab90f63575 | |||
150d271f79 | |||
a686eb2c9e | |||
bfa788935a | |||
64fa100a71 | |||
5fae987bfa | |||
9c4cda1c7d | |||
d9cbe56b63 | |||
86df7108b0 | |||
e7a4f92be7 | |||
76af452d29 | |||
11dfb685f4 | |||
d363aa0aa4 | |||
209d74c342 | |||
fec8f6febe | |||
ee9daaf4c8 | |||
4164369db4 | |||
747b6a61b8 | |||
9ed836c939 | |||
e4c82443f9 | |||
a2f232166b | |||
668f9ede65 | |||
b784829512 | |||
ad4fee7053 | |||
2d31e17251 | |||
39cff565ee | |||
203c5ba1b3 | |||
eda7bb5c3e | |||
1480a30050 | |||
d6087f75e2 | |||
dc084a5bdf |
@ -1,20 +1,42 @@
|
|||||||
|
defaults: &defaults
|
||||||
|
working_directory: ~/ng
|
||||||
|
docker:
|
||||||
|
- image: angular/ngcontainer
|
||||||
|
|
||||||
version: 2
|
version: 2
|
||||||
jobs:
|
jobs:
|
||||||
|
lint:
|
||||||
|
<<: *defaults
|
||||||
|
steps:
|
||||||
|
- checkout:
|
||||||
|
# After checkout, rebase on top of master.
|
||||||
|
# Similar to travis behavior, but not quite the same.
|
||||||
|
# See https://discuss.circleci.com/t/1662
|
||||||
|
post: git pull --ff-only origin "refs/pull/${CI_PULL_REQUEST//*pull\//}/merge"
|
||||||
|
- restore_cache:
|
||||||
|
key: angular-{{ .Branch }}-{{ checksum "npm-shrinkwrap.json" }}
|
||||||
|
|
||||||
|
- run: npm install
|
||||||
|
- run: npm run postinstall
|
||||||
|
- run: ./node_modules/.bin/gulp lint
|
||||||
|
|
||||||
build:
|
build:
|
||||||
working_directory: ~/ng
|
<<: *defaults
|
||||||
docker:
|
|
||||||
- image: alexeagle/ngcontainer
|
|
||||||
steps:
|
steps:
|
||||||
- checkout
|
- checkout
|
||||||
- restore_cache:
|
- restore_cache:
|
||||||
key: angular-{{ .Branch }}-{{ checksum "npm-shrinkwrap.json" }}
|
key: angular-{{ .Branch }}-{{ checksum "npm-shrinkwrap.json" }}
|
||||||
- run: npm install
|
|
||||||
- run: npm run postinstall
|
- run: bazel run @io_bazel_rules_typescript_node//:bin/npm install
|
||||||
- run: ./node_modules/.bin/gulp lint
|
- run: bazel build ...
|
||||||
# Build twice, workaround for
|
|
||||||
# https://github.com/bazelbuild/bazel/issues/3114
|
|
||||||
- run: bazel build ... || bazel build ...
|
|
||||||
- save_cache:
|
- save_cache:
|
||||||
key: angular-{{ .Branch }}-{{ checksum "npm-shrinkwrap.json" }}
|
key: angular-{{ .Branch }}-{{ checksum "npm-shrinkwrap.json" }}
|
||||||
paths:
|
paths:
|
||||||
- "node_modules"
|
- "node_modules"
|
||||||
|
|
||||||
|
workflows:
|
||||||
|
version: 2
|
||||||
|
default_workflow:
|
||||||
|
jobs:
|
||||||
|
- lint
|
||||||
|
- build
|
||||||
|
64
.github/ISSUE_TEMPLATE.md
vendored
64
.github/ISSUE_TEMPLATE.md
vendored
@ -1,39 +1,57 @@
|
|||||||
<!--
|
<!--
|
||||||
IF YOU DON'T FILL OUT THE FOLLOWING INFORMATION WE MIGHT CLOSE YOUR ISSUE WITHOUT INVESTIGATING
|
PLEASE HELP US PROCESS GITHUB ISSUES FASTER BY PROVIDING THE FOLLOWING INFORMATION.
|
||||||
|
|
||||||
|
ISSUES MISSING IMPORTANT INFORMATION MIGHT BE CLOSED WITHOUT INVESTIGATION.
|
||||||
-->
|
-->
|
||||||
|
|
||||||
**I'm submitting a ...** (check one with "x")
|
## I'm submitting a ...
|
||||||
```
|
<!-- Check one of the following options with "x" -->
|
||||||
[ ] bug report => search github for a similar issue or PR before submitting
|
<pre><code>
|
||||||
[ ] feature request
|
[ ] Regression (behavior that used to work and stopped working in a new release)
|
||||||
[ ] support request => Please do not submit support request here, instead see https://github.com/angular/angular/blob/master/CONTRIBUTING.md#question
|
[ ] Bug report <!-- Please search github for a similar issue or PR before submitting -->
|
||||||
```
|
[ ] Feature request
|
||||||
|
[ ] Documentation issue or request
|
||||||
|
[ ] Support request => Please do not submit support request here, instead see https://github.com/angular/angular/blob/master/CONTRIBUTING.md#question
|
||||||
|
</code></pre>
|
||||||
|
|
||||||
**Current behavior**
|
## Current behavior
|
||||||
<!-- Describe how the bug manifests. -->
|
<!-- Describe how the issue manifests. -->
|
||||||
|
|
||||||
**Expected behavior**
|
|
||||||
<!-- Describe what the behavior would be without the bug. -->
|
|
||||||
|
|
||||||
**Minimal reproduction of the problem with instructions**
|
## Expected behavior
|
||||||
|
<!-- Describe what the desired behavior would be. -->
|
||||||
|
|
||||||
|
|
||||||
|
## Minimal reproduction of the problem with instructions
|
||||||
<!--
|
<!--
|
||||||
If the current behavior is a bug or you can illustrate your feature request better with an example,
|
For bug reports please provide the *STEPS TO REPRODUCE* and if possible a *MINIMAL DEMO* of the problem via
|
||||||
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://plnkr.co or similar (you can use this template as a starting point: http://plnkr.co/edit/tpl:AvJOMERrnz94ekVua0u5).
|
||||||
-->
|
-->
|
||||||
|
|
||||||
**What is the motivation / use case for changing the behavior?**
|
## What is the motivation / use case for changing the behavior?
|
||||||
<!-- Describe the motivation or the concrete use case -->
|
<!-- Describe the motivation or the concrete use case. -->
|
||||||
|
|
||||||
**Please tell us about your environment:**
|
|
||||||
<!-- Operating system, IDE, package manager, HTTP server, ... -->
|
|
||||||
|
|
||||||
* **Angular version:** 2.0.X
|
## Please tell us about your environment
|
||||||
|
|
||||||
|
<pre><code>
|
||||||
|
Angular version: X.Y.Z
|
||||||
<!-- Check whether this is still an issue in the most recent Angular version -->
|
<!-- Check whether this is still an issue in the most recent Angular version -->
|
||||||
|
|
||||||
* **Browser:** [all | Chrome XX | Firefox XX | IE XX | Safari XX | Mobile Chrome XX | Android X.X Web Browser | iOS XX Safari | iOS XX UIWebView | iOS XX WKWebView ]
|
Browser:
|
||||||
<!-- All browsers where this could be reproduced -->
|
- [ ] Chrome (desktop) version XX
|
||||||
|
- [ ] Chrome (Android) version XX
|
||||||
|
- [ ] Chrome (iOS) version XX
|
||||||
|
- [ ] Firefox version XX
|
||||||
|
- [ ] Safari (desktop) version XX
|
||||||
|
- [ ] Safari (iOS) version XX
|
||||||
|
- [ ] IE version XX
|
||||||
|
- [ ] Edge version XX
|
||||||
|
|
||||||
* **Language:** [all | TypeScript X.X | ES6/7 | ES5]
|
For Tooling issues:
|
||||||
|
- Node version: XX <!-- use `node --version` -->
|
||||||
|
- Platform: <!-- Mac, Linux, Windows -->
|
||||||
|
|
||||||
* **Node (for AoT issues):** `node --version` =
|
Others:
|
||||||
|
<!-- Anything else relevant? Operating system version, IDE, package manager, HTTP server, ... -->
|
||||||
|
</code></pre>
|
||||||
|
27
.github/PULL_REQUEST_TEMPLATE.md
vendored
27
.github/PULL_REQUEST_TEMPLATE.md
vendored
@ -1,10 +1,15 @@
|
|||||||
**Please check if the PR fulfills these requirements**
|
## PR Checklist
|
||||||
|
Does please check if your PR fulfills the following requirements:
|
||||||
|
|
||||||
- [ ] The commit message follows our guidelines: https://github.com/angular/angular/blob/master/CONTRIBUTING.md#commit
|
- [ ] The commit message follows our guidelines: https://github.com/angular/angular/blob/master/CONTRIBUTING.md#commit
|
||||||
- [ ] Tests for the changes have been added (for bug fixes / features)
|
- [ ] Tests for the changes have been added (for bug fixes / features)
|
||||||
- [ ] Docs have been added / updated (for bug fixes / features)
|
- [ ] Docs have been added / updated (for bug fixes / features)
|
||||||
|
|
||||||
|
|
||||||
**What kind of change does this PR introduce?** (check one with "x")
|
## PR Type
|
||||||
|
What kind of change does this PR introduce?
|
||||||
|
|
||||||
|
<!-- Please check the one that applies to this PR using "x". -->
|
||||||
```
|
```
|
||||||
[ ] Bugfix
|
[ ] Bugfix
|
||||||
[ ] Feature
|
[ ] Feature
|
||||||
@ -12,25 +17,27 @@
|
|||||||
[ ] Refactoring (no functional changes, no api changes)
|
[ ] Refactoring (no functional changes, no api changes)
|
||||||
[ ] Build related changes
|
[ ] Build related changes
|
||||||
[ ] CI related changes
|
[ ] CI related changes
|
||||||
|
[ ] Documentation content changes
|
||||||
|
[ ] angular.io application / infrastructure changes
|
||||||
[ ] Other... Please describe:
|
[ ] Other... Please describe:
|
||||||
```
|
```
|
||||||
|
|
||||||
**What is the current behavior?** (You can also link to an open issue here)
|
## What is the current behavior?
|
||||||
|
<!-- Please describe the current behavior that you are modifying, or link to a relevant issue. -->
|
||||||
|
|
||||||
|
Issue Number: N/A
|
||||||
|
|
||||||
|
|
||||||
|
## What is the new behavior?
|
||||||
**What is the new behavior?**
|
|
||||||
|
|
||||||
|
|
||||||
|
## Does this PR introduce a breaking change?
|
||||||
**Does this PR introduce a breaking change?** (check one with "x")
|
|
||||||
```
|
```
|
||||||
[ ] Yes
|
[ ] Yes
|
||||||
[ ] No
|
[ ] No
|
||||||
```
|
```
|
||||||
|
|
||||||
If this PR contains a breaking change, please describe the impact and migration path for existing applications: ...
|
<!-- If this PR contains a breaking change, please describe the impact and migration path for existing applications below. -->
|
||||||
|
|
||||||
|
|
||||||
**Other information**:
|
## Other information
|
||||||
|
|
||||||
|
@ -8,9 +8,11 @@
|
|||||||
# alexeagle - Alex Eagle
|
# alexeagle - Alex Eagle
|
||||||
# alxhub - Alex Rickabaugh
|
# alxhub - Alex Rickabaugh
|
||||||
# chuckjaz - Chuck Jazdzewski
|
# chuckjaz - Chuck Jazdzewski
|
||||||
|
# Foxandxss - Jesús Rodríguez
|
||||||
# gkalpak - George Kalpakas
|
# gkalpak - George Kalpakas
|
||||||
# IgorMinar - Igor Minar
|
# IgorMinar - Igor Minar
|
||||||
# jasonaden - Jason Aden
|
# jasonaden - Jason Aden
|
||||||
|
# juleskremer - Jules Kremer
|
||||||
# kara - Kara Erickson
|
# kara - Kara Erickson
|
||||||
# matsko - Matias Niemelä
|
# matsko - Matias Niemelä
|
||||||
# mhevery - Misko Hevery
|
# mhevery - Misko Hevery
|
||||||
@ -18,10 +20,11 @@
|
|||||||
# pkozlowski-opensource - Pawel Kozlowski
|
# pkozlowski-opensource - Pawel Kozlowski
|
||||||
# robwormald - Rob Wormald
|
# robwormald - Rob Wormald
|
||||||
# tbosch - Tobias Bosch
|
# tbosch - Tobias Bosch
|
||||||
|
# tinayuangao - Tina Gao
|
||||||
# vicb - Victor Berchet
|
# vicb - Victor Berchet
|
||||||
# vikerman - Vikram Subramanian
|
# vikerman - Vikram Subramanian
|
||||||
# wardbell - Ward Bell
|
# wardbell - Ward Bell
|
||||||
# tinayuangao - Tina Gao
|
|
||||||
|
|
||||||
version: 2
|
version: 2
|
||||||
|
|
||||||
@ -93,6 +96,7 @@ groups:
|
|||||||
- "packages/core/*"
|
- "packages/core/*"
|
||||||
users:
|
users:
|
||||||
- tbosch #primary
|
- tbosch #primary
|
||||||
|
- chuckjaz
|
||||||
- mhevery
|
- mhevery
|
||||||
- vicb
|
- vicb
|
||||||
- IgorMinar #fallback
|
- IgorMinar #fallback
|
||||||
@ -104,6 +108,7 @@ 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
|
||||||
|
|
||||||
@ -249,10 +254,46 @@ groups:
|
|||||||
angular.io:
|
angular.io:
|
||||||
conditions:
|
conditions:
|
||||||
files:
|
files:
|
||||||
- "aio/*"
|
include:
|
||||||
|
- "aio/*"
|
||||||
|
exclude:
|
||||||
|
- "aio/content/*"
|
||||||
users:
|
users:
|
||||||
- IgorMinar #primary
|
- petebacondarwin #primary
|
||||||
- petebacondarwin #secondary
|
- IgorMinar
|
||||||
- gkalpak
|
- gkalpak
|
||||||
- wardbell
|
- mhevery #fallback
|
||||||
|
|
||||||
|
angular.io-guide-and-tutorial:
|
||||||
|
conditions:
|
||||||
|
files:
|
||||||
|
include:
|
||||||
|
- "aio/content/examples/*"
|
||||||
|
- "aio/content/guide/*"
|
||||||
|
- "aio/content/images/*"
|
||||||
|
- "aio/content/tutorial/*"
|
||||||
|
- "aio/content/file-not-found.md"
|
||||||
|
users:
|
||||||
|
- juleskremer #primary
|
||||||
|
- Foxandxss
|
||||||
|
- stephenfluin
|
||||||
|
- wardbell
|
||||||
|
- IgorMinar #fallback
|
||||||
|
- mhevery #fallback
|
||||||
|
|
||||||
|
angular.io-marketing:
|
||||||
|
conditions:
|
||||||
|
files:
|
||||||
|
include:
|
||||||
|
- "aio/content/*"
|
||||||
|
exclude:
|
||||||
|
- "aio/content/examples/*"
|
||||||
|
- "aio/content/guide/*"
|
||||||
|
- "aio/content/images/*"
|
||||||
|
- "aio/content/tutorial/*"
|
||||||
|
- "aio/content/file-not-found.md"
|
||||||
|
users:
|
||||||
|
- juleskremer #primary
|
||||||
|
- stephenfluin
|
||||||
|
- IgorMinar #fallback
|
||||||
- mhevery #fallback
|
- mhevery #fallback
|
||||||
|
@ -37,6 +37,10 @@ env:
|
|||||||
# This is needed for publishing builds to the "aio-staging" and "angular-io" firebase projects.
|
# This is needed for publishing builds to the "aio-staging" and "angular-io" firebase projects.
|
||||||
# This token was generated using the aio-deploy@angular.io account using `firebase login:ci` and password from valentine
|
# This token was generated using the aio-deploy@angular.io account using `firebase login:ci` and password from valentine
|
||||||
- secure: "L5CyQmpwWtoR4Qi4xlWQh/cL1M6ZeJL4W4QAr4HdKFMgYt9h+Whqkymyh2NxwmCbPvWa7yUd+OiLQUDCY7L2VIg16hTwoe2CgYDyQA0BEwLzxtRrJXl93TfwMlrUx5JSIzAccD6D4sjtz8kSFMomK2Nls33xOXOukwyhVMjd0Cg="
|
- secure: "L5CyQmpwWtoR4Qi4xlWQh/cL1M6ZeJL4W4QAr4HdKFMgYt9h+Whqkymyh2NxwmCbPvWa7yUd+OiLQUDCY7L2VIg16hTwoe2CgYDyQA0BEwLzxtRrJXl93TfwMlrUx5JSIzAccD6D4sjtz8kSFMomK2Nls33xOXOukwyhVMjd0Cg="
|
||||||
|
# ANGULAR_PAYLOAD_FIREBASE_TOKEN
|
||||||
|
# This is for payload size data to "angular-payload-size" firebase project
|
||||||
|
# This token was generated using the payload@angular.io account using `firebase login:ci` and password from valentine
|
||||||
|
- secure: "SxotP/ymNy6uWAVbfwM9BlwETPEBpkRvU/F7fCtQDDic99WfQHzzUSQqHTk8eKk3GrGAOSL09vT0WfStQYEIGEoS5UHWNgOnelxhw+d5EnaoB8vQ0dKQBTK092hQg4feFprr+B/tCasyMV6mVwpUzZMbIJNn/Rx7H5g1bp+Gkfg="
|
||||||
matrix:
|
matrix:
|
||||||
# Order: a slower build first, so that we don't occupy an idle travis worker waiting for others to complete.
|
# Order: a slower build first, so that we don't occupy an idle travis worker waiting for others to complete.
|
||||||
- CI_MODE=e2e
|
- CI_MODE=e2e
|
||||||
|
108
CHANGELOG.md
108
CHANGELOG.md
@ -1,3 +1,105 @@
|
|||||||
|
<a name="4.2.6"></a>
|
||||||
|
## [4.2.6](https://github.com/angular/angular/compare/4.2.5...4.2.6) (2017-07-08)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **animations:** ensure `:animating` queries collect previous animation elements properly ([d48b7d3](https://github.com/angular/angular/commit/d48b7d3))
|
||||||
|
* **animations:** properly cleanup query artificats when animation construction fails ([00de9ff](https://github.com/angular/angular/commit/00de9ff))
|
||||||
|
* **animations:** properly detect state transition changes for object literals ([00c9741](https://github.com/angular/angular/commit/00c9741))
|
||||||
|
* **animations:** properly handle cancelled animation style application ([cf57527](https://github.com/angular/angular/commit/cf57527))
|
||||||
|
* **compiler:** emits quoted keys only iff they are quoted in the original template ([45ae14c](https://github.com/angular/angular/commit/45ae14c)), closes [#14292](https://github.com/angular/angular/issues/14292)
|
||||||
|
* **compiler:** fix merge error ([6307581](https://github.com/angular/angular/commit/6307581))
|
||||||
|
* **compiler:** fix types ([5ea9b62](https://github.com/angular/angular/commit/5ea9b62))
|
||||||
|
* **compiler:** remove i18n markup even if no translations ([#17999](https://github.com/angular/angular/issues/17999)) ([2763577](https://github.com/angular/angular/commit/2763577)), closes [#11042](https://github.com/angular/angular/issues/11042)
|
||||||
|
* **compiler-cli:** fix relative source paths on windows for extracted msg ([#17915](https://github.com/angular/angular/issues/17915)) ([991f8ad](https://github.com/angular/angular/commit/991f8ad)), closes [#16639](https://github.com/angular/angular/issues/16639)
|
||||||
|
* **core:** fix re-insertions in the iterable differ ([#17891](https://github.com/angular/angular/issues/17891)) ([a318093](https://github.com/angular/angular/commit/a318093)), closes [#17852](https://github.com/angular/angular/issues/17852)
|
||||||
|
* **language-service:** do not crash when hovering over a label definitions ([#17974](https://github.com/angular/angular/issues/17974)) ([2ab9057](https://github.com/angular/angular/commit/2ab9057))
|
||||||
|
* **language-service:** ignore hover of symbols not in the TypeScript program ([#17969](https://github.com/angular/angular/issues/17969)) ([fe09e10](https://github.com/angular/angular/commit/fe09e10))
|
||||||
|
* **router:** encode URLs the same way AngularJS did (closer to spec) ([#17890](https://github.com/angular/angular/issues/17890)) ([8f7cce3](https://github.com/angular/angular/commit/8f7cce3)), closes [#16067](https://github.com/angular/angular/issues/16067)
|
||||||
|
* **router:** export missing UrlMatcher and UrlMatchResult types ([12a2099](https://github.com/angular/angular/commit/12a2099)), closes [#15140](https://github.com/angular/angular/issues/15140)
|
||||||
|
* **tsc-wrapped:** emit exports metadata in flat modules ([#17893](https://github.com/angular/angular/issues/17893)) ([ee7d134](https://github.com/angular/angular/commit/ee7d134))
|
||||||
|
* **upgrade:** fix transclusion on upgraded components ([#17971](https://github.com/angular/angular/issues/17971)) ([5337874](https://github.com/angular/angular/commit/5337874)), closes [#13271](https://github.com/angular/angular/issues/13271)
|
||||||
|
* **upgrade:** fix transclusion on upgraded components ([#17971](https://github.com/angular/angular/issues/17971)) ([30beb52](https://github.com/angular/angular/commit/30beb52)), closes [#13271](https://github.com/angular/angular/issues/13271)
|
||||||
|
|
||||||
|
|
||||||
|
### Performance Improvements
|
||||||
|
|
||||||
|
* **core:** refactor NgZone, decrease size by 1.2Kb ([#17773](https://github.com/angular/angular/issues/17773)) ([6d55a80](https://github.com/angular/angular/commit/6d55a80))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<a name="4.2.5"></a>
|
||||||
|
## [4.2.5](https://github.com/angular/angular/compare/4.2.4...4.2.5) (2017-06-30)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **animations:** do not delay style() values before a stagger() runs ([7559b78](https://github.com/angular/angular/commit/7559b78)), closes [#17412](https://github.com/angular/angular/issues/17412)
|
||||||
|
* **animations:** do not remove container nodes when children are queried by a parent animation ([ec4ae60](https://github.com/angular/angular/commit/ec4ae60)), closes [#17746](https://github.com/angular/angular/issues/17746)
|
||||||
|
* **animations:** do not validate style overlap errors in different transitions ([6909171](https://github.com/angular/angular/commit/6909171))
|
||||||
|
* **animations:** properly collect :enter nodes that exist within multi-level DOM trees ([79b6346](https://github.com/angular/angular/commit/79b6346)), closes [#17632](https://github.com/angular/angular/issues/17632)
|
||||||
|
* **core:** add needed closure compiler warning suppression ([f31b0d6](https://github.com/angular/angular/commit/f31b0d6))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<a name="4.3.0-beta.0"></a>
|
||||||
|
# [4.3.0-beta.0](https://github.com/angular/angular/compare/4.2.1...4.3.0-beta.0) (2017-06-22)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **animations:** compute removal node height correctly ([185075d](https://github.com/angular/angular/commit/185075d))
|
||||||
|
* **animations:** do not treat a `0` animation state as `void` ([451257a](https://github.com/angular/angular/commit/451257a))
|
||||||
|
* **animations:** properly collect :enter nodes in a partially updated collection ([6ca4692](https://github.com/angular/angular/commit/6ca4692)), closes [#17440](https://github.com/angular/angular/issues/17440)
|
||||||
|
* **animations:** remove duplicate license header ([e096a85](https://github.com/angular/angular/commit/e096a85))
|
||||||
|
* **compiler:** avoid emitting self importing factories ([4352dd2](https://github.com/angular/angular/commit/4352dd2))
|
||||||
|
* **compiler-cli:** find lazy routes in nested module import arrays ([8c89cc4](https://github.com/angular/angular/commit/8c89cc4))
|
||||||
|
* **core:** argument destructuring sometimes breaks strictNullChecks ([c59c390](https://github.com/angular/angular/commit/c59c390))
|
||||||
|
* **forms:** roll back breaking change with min/max directives ([3e685f9](https://github.com/angular/angular/commit/3e685f9)), closes [#17491](https://github.com/angular/angular/issues/17491)
|
||||||
|
* **language-service:** infer `any` `ngForOf` of type `any` ([f194f18](https://github.com/angular/angular/commit/f194f18))
|
||||||
|
* **language-service:** rollup `tslib` into the language service package ([4e6be15](https://github.com/angular/angular/commit/4e6be15))
|
||||||
|
* **router:** update the version placeholder so that it gets replaced during the build ([d3c92a3](https://github.com/angular/angular/commit/d3c92a3)), closes [#17403](https://github.com/angular/angular/issues/17403)
|
||||||
|
* **tsc-wrapped:** skip collecting metadata for default functions ([46ddf50](https://github.com/angular/angular/commit/46ddf50))
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **core:** update zone.js to 0.8.12 ([5ac3919](https://github.com/angular/angular/commit/5ac3919))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<a name="4.2.4"></a>
|
||||||
|
## [4.2.4](https://github.com/angular/angular/compare/4.2.3...4.2.4) (2017-06-21)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **compiler:** avoid emitting self importing factories ([c112232](https://github.com/angular/angular/commit/c112232))
|
||||||
|
* **compiler-cli:** find lazy routes in nested module import arrays ([59299de](https://github.com/angular/angular/commit/59299de))
|
||||||
|
* **core**: argument destructuring sometimes breaks strictNullChecks ([77860a0](https://github.com/angular/angular/commit/77860a0))
|
||||||
|
* **forms:** roll back breaking change with min/max directives ([4ab7353](https://github.com/angular/angular/commit/4ab7353)), closes [#17491](https://github.com/angular/angular/issues/17491)
|
||||||
|
* **language-service:** infer `any` `ngForOf` of type `any` ([63a5f33](https://github.com/angular/angular/commit/63a5f33))
|
||||||
|
* **language-service:** rollup `tslib` into the language service package ([20eb5cf](https://github.com/angular/angular/commit/20eb5cf))
|
||||||
|
* **router:** update the version placeholder so that it gets replaced during the build ([7de1ae2](https://github.com/angular/angular/commit/7de1ae2)), closes [#17403](https://github.com/angular/angular/issues/17403)
|
||||||
|
* **tsc-wrapped:** skip collecting metadata for default functions ([3390648](https://github.com/angular/angular/commit/3390648))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<a name="4.2.3"></a>
|
||||||
|
## [4.2.3](https://github.com/angular/angular/compare/4.2.1...4.2.3) (2017-06-16)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **animations:** compute removal node height correctly ([185075d](https://github.com/angular/angular/commit/185075d))
|
||||||
|
* **animations:** do not treat a `0` animation state as `void` ([451257a](https://github.com/angular/angular/commit/451257a))
|
||||||
|
* **animations:** properly collect :enter nodes in a partially updated collection ([6ca4692](https://github.com/angular/angular/commit/6ca4692)), closes [#17440](https://github.com/angular/angular/issues/17440)
|
||||||
|
* **animations:** remove duplicate license header ([b192dd5](https://github.com/angular/angular/commit/b192dd5))
|
||||||
|
* **forms:** temp roll back breaking change with min/max directives ([b8c39cd](https://github.com/angular/angular/commit/b8c39cd)), closes [#17491](https://github.com/angular/angular/issues/17491)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<a name="4.2.2"></a>
|
<a name="4.2.2"></a>
|
||||||
## [4.2.2](https://github.com/angular/angular/compare/4.2.1...4.2.2) (2017-06-12)
|
## [4.2.2](https://github.com/angular/angular/compare/4.2.1...4.2.2) (2017-06-12)
|
||||||
|
|
||||||
@ -97,7 +199,7 @@
|
|||||||
|
|
||||||
### Bug Fixes
|
### Bug Fixes
|
||||||
|
|
||||||
* **animations:** make sure reuseable animation subtitutions work without default params ([#16875](https://github.com/angular/angular/issues/16875)) ([7d9f96a](https://github.com/angular/angular/commit/7d9f96a))
|
* **animations:** make sure reusable animation substitutions work without default params ([#16875](https://github.com/angular/angular/issues/16875)) ([7d9f96a](https://github.com/angular/angular/commit/7d9f96a))
|
||||||
* **animations:** only require one flushMicrotasks call when testing animations ([6cb93c1](https://github.com/angular/angular/commit/6cb93c1))
|
* **animations:** only require one flushMicrotasks call when testing animations ([6cb93c1](https://github.com/angular/angular/commit/6cb93c1))
|
||||||
* **compiler:** avoid a `...null` spread in extraction ([#16547](https://github.com/angular/angular/issues/16547)) ([e0a8376](https://github.com/angular/angular/commit/e0a8376))
|
* **compiler:** avoid a `...null` spread in extraction ([#16547](https://github.com/angular/angular/issues/16547)) ([e0a8376](https://github.com/angular/angular/commit/e0a8376))
|
||||||
* **compiler-cli:** allow '==' to compare nullable types ([#16731](https://github.com/angular/angular/issues/16731)) ([d761059](https://github.com/angular/angular/commit/d761059))
|
* **compiler-cli:** allow '==' to compare nullable types ([#16731](https://github.com/angular/angular/issues/16731)) ([d761059](https://github.com/angular/angular/commit/d761059))
|
||||||
@ -112,7 +214,7 @@
|
|||||||
### Features
|
### Features
|
||||||
|
|
||||||
* **animations:** introduce a wave of new animation features ([16c8167](https://github.com/angular/angular/commit/16c8167))
|
* **animations:** introduce a wave of new animation features ([16c8167](https://github.com/angular/angular/commit/16c8167))
|
||||||
* **animations:** introduce routeable animation support ([f1a9e3c](https://github.com/angular/angular/commit/f1a9e3c))
|
* **animations:** introduce routable animation support ([f1a9e3c](https://github.com/angular/angular/commit/f1a9e3c))
|
||||||
* add .ngsummary.ts files to support AOT unit tests ([547c363](https://github.com/angular/angular/commit/547c363))
|
* add .ngsummary.ts files to support AOT unit tests ([547c363](https://github.com/angular/angular/commit/547c363))
|
||||||
* introduce `TestBed.overrideProvider` ([#16725](https://github.com/angular/angular/issues/16725)) ([39b92f7](https://github.com/angular/angular/commit/39b92f7))
|
* introduce `TestBed.overrideProvider` ([#16725](https://github.com/angular/angular/issues/16725)) ([39b92f7](https://github.com/angular/angular/commit/39b92f7))
|
||||||
* **compiler:** support a non-null postfix assert ([#16672](https://github.com/angular/angular/issues/16672)) ([b9521b5](https://github.com/angular/angular/commit/b9521b5))
|
* **compiler:** support a non-null postfix assert ([#16672](https://github.com/angular/angular/issues/16672)) ([b9521b5](https://github.com/angular/angular/commit/b9521b5))
|
||||||
@ -637,7 +739,7 @@ Please give this RC a try and test it with your projects! We have spent a signif
|
|||||||
## What’s New
|
## What’s New
|
||||||
|
|
||||||
### View Engine
|
### View Engine
|
||||||
We’ve made changes under to hood to what AOT generated code looks like. These changes should reduce the size of the generated code for your components by more than half in some cases. Read the [Design Doc](https://docs.google.com/document/d/195L4WaDSoI_kkW094LlShH6gT3B7K1GZpSBnnLkQR-g/preview) for the View Engine updates.
|
We’ve made changes under the hood to what AOT generated code looks like. These changes should reduce the size of the generated code for your components by more than half in some cases. Read the [Design Doc](https://docs.google.com/document/d/195L4WaDSoI_kkW094LlShH6gT3B7K1GZpSBnnLkQR-g/preview) for the View Engine updates.
|
||||||
|
|
||||||
|
|
||||||
### Enhanced `*ngIf` syntax
|
### Enhanced `*ngIf` syntax
|
||||||
|
@ -17,15 +17,15 @@ Help us keep Angular open and inclusive. Please read and follow our [Code of Con
|
|||||||
|
|
||||||
## <a name="question"></a> Got a Question or Problem?
|
## <a name="question"></a> Got a Question or Problem?
|
||||||
|
|
||||||
Please, do not open issues for the general support questions as we want to keep GitHub issues for bug reports and feature requests. You've got much better chances of getting your question answered on [StackOverflow](https://stackoverflow.com/questions/tagged/angular) where the questions should be tagged with tag `angular`.
|
Please, do not open issues for the general support questions as we want to keep GitHub issues for bug reports and feature requests. You've got much better chances of getting your question answered on [Stack Overflow](https://stackoverflow.com/questions/tagged/angular) where the questions should be tagged with tag `angular`.
|
||||||
|
|
||||||
StackOverflow is a much better place to ask questions since:
|
Stack Overflow is a much better place to ask questions since:
|
||||||
|
|
||||||
- there are thousands of people willing to help on StackOverflow
|
- there are thousands of people willing to help on Stack Overflow
|
||||||
- questions and answers stay available for public viewing so your question / answer might help someone else
|
- questions and answers stay available for public viewing so your question / answer might help someone else
|
||||||
- StackOverflow's voting system assures that the best answers are prominently visible.
|
- Stack Overflow's voting system assures that the best answers are prominently visible.
|
||||||
|
|
||||||
To save your and our time we will be systematically closing all the issues that are requests for general support and redirecting people to StackOverflow.
|
To save your and our time we will be systematically closing all the issues that are requests for general support and redirecting people to Stack Overflow.
|
||||||
|
|
||||||
If you would like to chat about the question in real-time, you can reach out via [our gitter channel][gitter].
|
If you would like to chat about the question in real-time, you can reach out via [our gitter channel][gitter].
|
||||||
|
|
||||||
@ -198,8 +198,7 @@ Must be one of the following:
|
|||||||
* **fix**: A bug fix
|
* **fix**: A bug fix
|
||||||
* **perf**: A code change that improves performance
|
* **perf**: A code change that improves performance
|
||||||
* **refactor**: A code change that neither fixes a bug nor adds a feature
|
* **refactor**: A code change that neither fixes a bug nor adds a feature
|
||||||
* **style**: Changes that do not affect the meaning of the code (white-space, formatting, missing
|
* **style**: Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc)
|
||||||
semi-colons, etc)
|
|
||||||
* **test**: Adding missing tests or correcting existing tests
|
* **test**: Adding missing tests or correcting existing tests
|
||||||
|
|
||||||
### Scope
|
### Scope
|
||||||
@ -207,6 +206,7 @@ The scope should be the name of the npm package affected (as perceived by person
|
|||||||
|
|
||||||
The following is the list of supported scopes:
|
The following is the list of supported scopes:
|
||||||
|
|
||||||
|
* **animations**
|
||||||
* **common**
|
* **common**
|
||||||
* **compiler**
|
* **compiler**
|
||||||
* **compiler-cli**
|
* **compiler-cli**
|
||||||
@ -223,7 +223,7 @@ The following is the list of supported scopes:
|
|||||||
* **upgrade**
|
* **upgrade**
|
||||||
* **tsc-wrapped**
|
* **tsc-wrapped**
|
||||||
|
|
||||||
There is currently few exception to the "use package name" rule:
|
There are currently a few exceptions to the "use package name" rule:
|
||||||
|
|
||||||
* **packaging**: used for changes that change the npm package layout in all of our packages, e.g. public path changes, package.json changes done to all packages, d.ts file/format changes, changes to bundles, etc.
|
* **packaging**: used for changes that change the npm package layout in all of our packages, e.g. public path changes, package.json changes done to all packages, d.ts file/format changes, changes to bundles, etc.
|
||||||
* **changelog**: used for updating the release notes in CHANGELOG.md
|
* **changelog**: used for updating the release notes in CHANGELOG.md
|
||||||
|
@ -3,7 +3,8 @@
|
|||||||
[](https://gitter.im/angular/angular?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
[](https://gitter.im/angular/angular?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
||||||
[](http://issuestats.com/github/angular/angular)
|
[](http://issuestats.com/github/angular/angular)
|
||||||
[](http://issuestats.com/github/angular/angular)
|
[](http://issuestats.com/github/angular/angular)
|
||||||
[](https://badge.fury.io/js/%40angular%2Fcore)
|
[](https://www.npmjs.com/@angular/core)
|
||||||
|
)
|
||||||
|
|
||||||
[](https://saucelabs.com/u/angular2-ci)
|
[](https://saucelabs.com/u/angular2-ci)
|
||||||
*Safari (7+), iOS (7+), Edge (14) and IE mobile (11) are tested on [BrowserStack][browserstack].*
|
*Safari (7+), iOS (7+), Edge (14) and IE mobile (11) are tested on [BrowserStack][browserstack].*
|
||||||
|
11
WORKSPACE
11
WORKSPACE
@ -1,12 +1,11 @@
|
|||||||
load("@bazel_tools//tools/build_defs/repo:git.bzl", "git_repository")
|
load("@bazel_tools//tools/build_defs/repo:git.bzl", "git_repository")
|
||||||
|
|
||||||
git_repository(
|
git_repository(
|
||||||
name = "io_bazel_rules_typescript",
|
name = "io_bazel_rules_typescript",
|
||||||
remote = "https://github.com/bazelbuild/rules_typescript.git",
|
remote = "https://github.com/bazelbuild/rules_typescript.git",
|
||||||
tag = "0.0.3",
|
commit = "3a8404d",
|
||||||
)
|
)
|
||||||
|
|
||||||
load("@io_bazel_rules_typescript//:defs.bzl", "node_repositories", "yarn_install")
|
load("@io_bazel_rules_typescript//:defs.bzl", "node_repositories")
|
||||||
|
|
||||||
node_repositories()
|
node_repositories(package_json = "//:package.json")
|
||||||
yarn_install(package_json = "//:package.json")
|
|
||||||
|
@ -31,6 +31,7 @@
|
|||||||
"environmentSource": "environments/environment.ts",
|
"environmentSource": "environments/environment.ts",
|
||||||
"environments": {
|
"environments": {
|
||||||
"dev": "environments/environment.ts",
|
"dev": "environments/environment.ts",
|
||||||
|
"stage": "environments/environment.stage.ts",
|
||||||
"prod": "environments/environment.prod.ts"
|
"prod": "environments/environment.prod.ts"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
3
aio/.gitignore
vendored
3
aio/.gitignore
vendored
@ -43,3 +43,6 @@ protractor-results*.txt
|
|||||||
# System Files
|
# System Files
|
||||||
.DS_Store
|
.DS_Store
|
||||||
Thumbs.db
|
Thumbs.db
|
||||||
|
|
||||||
|
# copied dependencies
|
||||||
|
src/assets/js/lunr*
|
@ -16,6 +16,7 @@ Here are the most important tasks you might need to use:
|
|||||||
* `yarn setup` - Install all the dependencies, boilerplate, plunkers, zips and runs dgeni on the docs.
|
* `yarn setup` - Install all the dependencies, boilerplate, plunkers, zips and runs dgeni on the docs.
|
||||||
|
|
||||||
* `yarn start` - run a development web server that watches the files; then builds the doc-viewer and reloads the page, as necessary.
|
* `yarn start` - run a development web server that watches the files; then builds the doc-viewer and reloads the page, as necessary.
|
||||||
|
* `yarn serve-and-sync` - run both the `docs-watch` and `start` in the same console.
|
||||||
* `yarn lint` - check that the doc-viewer code follows our style rules.
|
* `yarn lint` - check that the doc-viewer code follows our style rules.
|
||||||
* `yarn test` - watch all the source files, for the doc-viewer, and run all the unit tests when any change.
|
* `yarn test` - watch all the source files, for the doc-viewer, and run all the unit tests when any change.
|
||||||
* `yarn e2e` - run all the e2e tests for the doc-viewer.
|
* `yarn e2e` - run all the e2e tests for the doc-viewer.
|
||||||
@ -30,6 +31,10 @@ Here are the most important tasks you might need to use:
|
|||||||
* `yarn generate-plunkers` - generate the plunker files that are used by the `live-example` tags in the docs.
|
* `yarn generate-plunkers` - generate the plunker 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 -- --setup` - force webdriver update & other setup, then run tests
|
||||||
|
- `yarn example-e2e -- --filter=foo` - limit e2e tests to those containing the word "foo"
|
||||||
|
|
||||||
* `yarn build-ie-polyfills` - generates a js file of polyfills that can be loaded in Internet Explorer.
|
* `yarn build-ie-polyfills` - generates a js file of polyfills that can be loaded in Internet Explorer.
|
||||||
|
|
||||||
## Using ServiceWorker locally
|
## Using ServiceWorker locally
|
||||||
@ -60,6 +65,9 @@ More specifically, there are sub-folders that contain particular types of conten
|
|||||||
|
|
||||||
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
|
||||||
|
writing guide pages, explains how to use the documentation classes and components, and how to markup sample source code to produce code snippets.
|
||||||
|
|
||||||
### Generating the complete docs
|
### Generating the complete docs
|
||||||
|
|
||||||
The main task for generating the docs is `yarn docs`. This will process all the source files (API and other),
|
The main task for generating the docs is `yarn docs`. This will process all the source files (API and other),
|
||||||
|
@ -29,6 +29,8 @@ 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_REPO_SLUG=angular/angular
|
||||||
ARG TEST_AIO_REPO_SLUG=test-repo/test-slug
|
ARG TEST_AIO_REPO_SLUG=test-repo/test-slug
|
||||||
|
ARG AIO_TRUSTED_PR_LABEL="aio: preview"
|
||||||
|
ARG TEST_AIO_TRUSTED_PR_LABEL="aio: preview"
|
||||||
ARG AIO_UPLOAD_HOSTNAME=upload.localhost
|
ARG AIO_UPLOAD_HOSTNAME=upload.localhost
|
||||||
ARG TEST_AIO_UPLOAD_HOSTNAME=upload.localhost
|
ARG TEST_AIO_UPLOAD_HOSTNAME=upload.localhost
|
||||||
ARG AIO_UPLOAD_MAX_SIZE=20971520
|
ARG AIO_UPLOAD_MAX_SIZE=20971520
|
||||||
@ -48,6 +50,7 @@ ENV AIO_BUILDS_DIR=$AIO_BUILDS_DIR TEST_AIO_BUILDS_DIR=$TEST
|
|||||||
AIO_REPO_SLUG=$AIO_REPO_SLUG TEST_AIO_REPO_SLUG=$TEST_AIO_REPO_SLUG \
|
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_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_UPLOAD_HOSTNAME=$AIO_UPLOAD_HOSTNAME TEST_AIO_UPLOAD_HOSTNAME=$TEST_AIO_UPLOAD_HOSTNAME \
|
||||||
AIO_UPLOAD_MAX_SIZE=$AIO_UPLOAD_MAX_SIZE TEST_AIO_UPLOAD_MAX_SIZE=$TEST_AIO_UPLOAD_MAX_SIZE \
|
AIO_UPLOAD_MAX_SIZE=$AIO_UPLOAD_MAX_SIZE TEST_AIO_UPLOAD_MAX_SIZE=$TEST_AIO_UPLOAD_MAX_SIZE \
|
||||||
AIO_UPLOAD_PORT=$AIO_UPLOAD_PORT TEST_AIO_UPLOAD_PORT=$TEST_AIO_UPLOAD_PORT \
|
AIO_UPLOAD_PORT=$AIO_UPLOAD_PORT TEST_AIO_UPLOAD_PORT=$TEST_AIO_UPLOAD_PORT \
|
||||||
|
@ -15,7 +15,7 @@ server {
|
|||||||
|
|
||||||
# Serve PR-preview requests
|
# Serve PR-preview requests
|
||||||
server {
|
server {
|
||||||
server_name "~^pr(?<pr>[1-9][0-9]*)-(?<sha>[0-9a-f]{40})\.";
|
server_name "~^pr(?<pr>[1-9][0-9]*)-(?<sha>[0-9a-f]{7,40})\.";
|
||||||
|
|
||||||
listen {{$AIO_NGINX_PORT_HTTPS}} ssl http2;
|
listen {{$AIO_NGINX_PORT_HTTPS}} ssl http2;
|
||||||
listen [::]:{{$AIO_NGINX_PORT_HTTPS}} ssl http2;
|
listen [::]:{{$AIO_NGINX_PORT_HTTPS}} ssl http2;
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
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} from '../common/constants';
|
||||||
import {GithubPullRequests} from '../common/github-pull-requests';
|
import {GithubPullRequests} from '../common/github-pull-requests';
|
||||||
import {assertNotMissingOrEmpty} from '../common/utils';
|
import {assertNotMissingOrEmpty} from '../common/utils';
|
||||||
|
|
||||||
@ -31,8 +32,9 @@ export class BuildCleaner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const buildNumbers = files.
|
const buildNumbers = files.
|
||||||
map(Number). // Convert string to number
|
map(name => name.replace(HIDDEN_DIR_PREFIX, '')). // Remove the "hidden dir" prefix
|
||||||
filter(Boolean); // Ignore NaN (or 0), because they are not builds
|
map(Number). // Convert string to number
|
||||||
|
filter(Boolean); // Ignore NaN (or 0), because they are not builds
|
||||||
|
|
||||||
resolve(buildNumbers);
|
resolve(buildNumbers);
|
||||||
});
|
});
|
||||||
@ -49,9 +51,11 @@ export class BuildCleaner {
|
|||||||
|
|
||||||
protected removeDir(dir: string) {
|
protected removeDir(dir: string) {
|
||||||
try {
|
try {
|
||||||
// Undocumented signature (see https://github.com/shelljs/shelljs/pull/663).
|
if (shell.test('-d', dir)) {
|
||||||
(shell as any).chmod('-R', 'a+w', dir);
|
// Undocumented signature (see https://github.com/shelljs/shelljs/pull/663).
|
||||||
shell.rm('-rf', dir);
|
(shell as any).chmod('-R', 'a+w', dir);
|
||||||
|
shell.rm('-rf', dir);
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(`ERROR: Unable to remove '${dir}' due to:`, err);
|
console.error(`ERROR: Unable to remove '${dir}' due to:`, err);
|
||||||
}
|
}
|
||||||
@ -64,8 +68,14 @@ export class BuildCleaner {
|
|||||||
console.log(`Open pull requests: ${openPrNumbers.length}`);
|
console.log(`Open pull requests: ${openPrNumbers.length}`);
|
||||||
console.log(`Removing ${toRemove.length} build(s): ${toRemove.join(', ')}`);
|
console.log(`Removing ${toRemove.length} build(s): ${toRemove.join(', ')}`);
|
||||||
|
|
||||||
|
// Try removing public dirs.
|
||||||
toRemove.
|
toRemove.
|
||||||
map(num => path.join(this.buildsDir, String(num))).
|
map(num => path.join(this.buildsDir, String(num))).
|
||||||
forEach(dir => this.removeDir(dir));
|
forEach(dir => this.removeDir(dir));
|
||||||
|
|
||||||
|
// Try removing hidden dirs.
|
||||||
|
toRemove.
|
||||||
|
map(num => path.join(this.buildsDir, HIDDEN_DIR_PREFIX + String(num))).
|
||||||
|
forEach(dir => this.removeDir(dir));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,3 @@
|
|||||||
|
// Constants
|
||||||
|
export const HIDDEN_DIR_PREFIX = 'hidden--';
|
||||||
|
export const SHORT_SHA_LEN = 7;
|
@ -63,7 +63,7 @@ export class GithubApi {
|
|||||||
return items;
|
return items;
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.getPaginated(pathname, baseParams, currentPage + 1).then(moreItems => [...items, ...moreItems]);
|
return this.getPaginated<T>(pathname, baseParams, currentPage + 1).then(moreItems => [...items, ...moreItems]);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -6,6 +6,7 @@ import {GithubApi} from './github-api';
|
|||||||
export interface PullRequest {
|
export interface PullRequest {
|
||||||
number: number;
|
number: number;
|
||||||
user: {login: string};
|
user: {login: string};
|
||||||
|
labels: {name: string}[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export type PullRequestState = 'all' | 'closed' | 'open';
|
export type PullRequestState = 'all' | 'closed' | 'open';
|
||||||
@ -30,7 +31,8 @@ export class GithubPullRequests extends GithubApi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public fetch(pr: number): Promise<PullRequest> {
|
public fetch(pr: number): Promise<PullRequest> {
|
||||||
return this.get<PullRequest>(`/repos/${this.repoSlug}/pulls/${pr}`);
|
// Using the `/issues/` URL, because the `/pulls/` one does not provide labels.
|
||||||
|
return this.get<PullRequest>(`/repos/${this.repoSlug}/issues/${pr}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
public fetchAll(state: PullRequestState = 'all'): Promise<PullRequest[]> {
|
public fetchAll(state: PullRequestState = 'all'): Promise<PullRequest[]> {
|
||||||
|
@ -4,8 +4,9 @@ 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 {assertNotMissingOrEmpty} from '../common/utils';
|
import {assertNotMissingOrEmpty} from '../common/utils';
|
||||||
import {CreatedBuildEvent} from './build-events';
|
import {ChangedPrVisibilityEvent, CreatedBuildEvent} from './build-events';
|
||||||
import {UploadError} from './upload-error';
|
import {UploadError} from './upload-error';
|
||||||
|
|
||||||
// Classes
|
// Classes
|
||||||
@ -17,13 +18,46 @@ export class BuildCreator extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Methods - Public
|
// Methods - Public
|
||||||
public create(pr: string, sha: string, archivePath: string): Promise<any> {
|
public changePrVisibility(pr: string, makePublic: boolean): Promise<void> {
|
||||||
const prDir = path.join(this.buildsDir, pr);
|
const {oldPrDir, newPrDir} = this.getCandidatePrDirs(pr, makePublic);
|
||||||
|
|
||||||
|
return Promise.
|
||||||
|
all([this.exists(oldPrDir), this.exists(newPrDir)]).
|
||||||
|
then(([oldPrDirExisted, newPrDirExisted]) => {
|
||||||
|
if (!oldPrDirExisted) {
|
||||||
|
throw new UploadError(404, `Request to move non-existing directory '${oldPrDir}' to '${newPrDir}'.`);
|
||||||
|
} else if (newPrDirExisted) {
|
||||||
|
throw new UploadError(409, `Request to move '${oldPrDir}' to existing directory '${newPrDir}'.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.resolve().
|
||||||
|
then(() => shell.mv(oldPrDir, newPrDir)).
|
||||||
|
then(() => this.listShasByDate(newPrDir)).
|
||||||
|
then(shas => this.emit(ChangedPrVisibilityEvent.type, new ChangedPrVisibilityEvent(+pr, shas, makePublic))).
|
||||||
|
then(() => undefined);
|
||||||
|
}).
|
||||||
|
catch(err => {
|
||||||
|
if (!(err instanceof UploadError)) {
|
||||||
|
err = new UploadError(500, `Error while making PR ${pr} ${makePublic ? 'public' : 'hidden'}.\n${err}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw err;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public create(pr: string, sha: string, archivePath: string, isPublic: boolean): Promise<void> {
|
||||||
|
// Use only part of the SHA for more readable URLs.
|
||||||
|
sha = sha.substr(0, SHORT_SHA_LEN);
|
||||||
|
|
||||||
|
const {oldPrDir: otherVisPrDir, newPrDir: prDir} = this.getCandidatePrDirs(pr, isPublic);
|
||||||
const shaDir = path.join(prDir, sha);
|
const shaDir = path.join(prDir, sha);
|
||||||
let dirToRemoveOnError: string;
|
let dirToRemoveOnError: string;
|
||||||
|
|
||||||
return Promise.
|
return Promise.resolve().
|
||||||
all([this.exists(prDir), this.exists(shaDir)]).
|
then(() => this.exists(otherVisPrDir)).
|
||||||
|
// If the same PR exists with different visibility, update the visibility first.
|
||||||
|
then(otherVisPrDirExisted => (otherVisPrDirExisted && this.changePrVisibility(pr, isPublic)) as any).
|
||||||
|
then(() => Promise.all([this.exists(prDir), this.exists(shaDir)])).
|
||||||
then(([prDirExisted, shaDirExisted]) => {
|
then(([prDirExisted, shaDirExisted]) => {
|
||||||
if (shaDirExisted) {
|
if (shaDirExisted) {
|
||||||
throw new UploadError(409, `Request to overwrite existing directory: ${shaDir}`);
|
throw new UploadError(409, `Request to overwrite existing directory: ${shaDir}`);
|
||||||
@ -34,7 +68,8 @@ export class BuildCreator extends EventEmitter {
|
|||||||
return Promise.resolve().
|
return Promise.resolve().
|
||||||
then(() => shell.mkdir('-p', shaDir)).
|
then(() => shell.mkdir('-p', shaDir)).
|
||||||
then(() => this.extractArchive(archivePath, shaDir)).
|
then(() => this.extractArchive(archivePath, shaDir)).
|
||||||
then(() => this.emit(CreatedBuildEvent.type, new CreatedBuildEvent(+pr, sha)));
|
then(() => this.emit(CreatedBuildEvent.type, new CreatedBuildEvent(+pr, sha, isPublic))).
|
||||||
|
then(() => undefined);
|
||||||
}).
|
}).
|
||||||
catch(err => {
|
catch(err => {
|
||||||
if (dirToRemoveOnError) {
|
if (dirToRemoveOnError) {
|
||||||
@ -78,4 +113,26 @@ export class BuildCreator extends EventEmitter {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected getCandidatePrDirs(pr: string, isPublic: boolean) {
|
||||||
|
const hiddenPrDir = path.join(this.buildsDir, HIDDEN_DIR_PREFIX + pr);
|
||||||
|
const publicPrDir = path.join(this.buildsDir, pr);
|
||||||
|
|
||||||
|
const oldPrDir = isPublic ? hiddenPrDir : publicPrDir;
|
||||||
|
const newPrDir = isPublic ? publicPrDir : hiddenPrDir;
|
||||||
|
|
||||||
|
return {oldPrDir, newPrDir};
|
||||||
|
}
|
||||||
|
|
||||||
|
protected listShasByDate(inputDir: string): Promise<string[]> {
|
||||||
|
return Promise.resolve().
|
||||||
|
then(() => shell.ls('-l', inputDir) as any as Promise<(fs.Stats & {name: string})[]>).
|
||||||
|
// Keep directories only.
|
||||||
|
// (Also, convert to standard Array - ShellJS provides custom `sort()` method for sorting file contents.)
|
||||||
|
then(items => items.filter(item => item.isDirectory())).
|
||||||
|
// Sort by modification date.
|
||||||
|
then(items => items.sort((a, b) => a.mtime.getTime() - b.mtime.getTime())).
|
||||||
|
// Return directory names.
|
||||||
|
then(items => items.map(item => item.name));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,15 +1,16 @@
|
|||||||
// Classes
|
// Classes
|
||||||
export class BuildEvent {
|
export class ChangedPrVisibilityEvent {
|
||||||
|
// Properties - Public, Static
|
||||||
|
public static type = 'pr.changedVisibility';
|
||||||
|
|
||||||
// Constructor
|
// Constructor
|
||||||
constructor(public type: string, public pr: number, public sha: string) {}
|
constructor(public pr: number, public shas: string[], public isPublic: boolean) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class CreatedBuildEvent extends BuildEvent {
|
export class CreatedBuildEvent {
|
||||||
// Properties - Public, Static
|
// Properties - Public, Static
|
||||||
public static type = 'build.created';
|
public static type = 'build.created';
|
||||||
|
|
||||||
// Constructor
|
// Constructor
|
||||||
constructor(pr: number, sha: string) {
|
constructor(public pr: number, public sha: string, public isPublic: boolean) {}
|
||||||
super(CreatedBuildEvent.type, pr, sha);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
// Imports
|
// Imports
|
||||||
import * as jwt from 'jsonwebtoken';
|
import * as jwt from 'jsonwebtoken';
|
||||||
import {GithubPullRequests} from '../common/github-pull-requests';
|
import {GithubPullRequests, PullRequest} from '../common/github-pull-requests';
|
||||||
import {GithubTeams} from '../common/github-teams';
|
import {GithubTeams} from '../common/github-teams';
|
||||||
import {assertNotMissingOrEmpty} from '../common/utils';
|
import {assertNotMissingOrEmpty} from '../common/utils';
|
||||||
import {UploadError} from './upload-error';
|
import {UploadError} from './upload-error';
|
||||||
@ -11,6 +11,12 @@ interface JwtPayload {
|
|||||||
'pull-request': number;
|
'pull-request': number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Enums
|
||||||
|
export enum BUILD_VERIFICATION_STATUS {
|
||||||
|
verifiedAndTrusted,
|
||||||
|
verifiedNotTrusted,
|
||||||
|
}
|
||||||
|
|
||||||
// Classes
|
// Classes
|
||||||
export class BuildVerifier {
|
export class BuildVerifier {
|
||||||
// Properties - Protected
|
// Properties - Protected
|
||||||
@ -19,27 +25,27 @@ export class BuildVerifier {
|
|||||||
|
|
||||||
// Constructor
|
// Constructor
|
||||||
constructor(protected secret: string, githubToken: string, protected repoSlug: string, organization: string,
|
constructor(protected secret: string, githubToken: string, protected repoSlug: string, organization: string,
|
||||||
protected allowedTeamSlugs: string[]) {
|
protected allowedTeamSlugs: string[], protected trustedPrLabel: string) {
|
||||||
assertNotMissingOrEmpty('secret', secret);
|
assertNotMissingOrEmpty('secret', secret);
|
||||||
assertNotMissingOrEmpty('githubToken', githubToken);
|
assertNotMissingOrEmpty('githubToken', githubToken);
|
||||||
assertNotMissingOrEmpty('repoSlug', repoSlug);
|
assertNotMissingOrEmpty('repoSlug', repoSlug);
|
||||||
assertNotMissingOrEmpty('organization', organization);
|
assertNotMissingOrEmpty('organization', organization);
|
||||||
assertNotMissingOrEmpty('allowedTeamSlugs', allowedTeamSlugs && allowedTeamSlugs.join(''));
|
assertNotMissingOrEmpty('allowedTeamSlugs', allowedTeamSlugs && allowedTeamSlugs.join(''));
|
||||||
|
assertNotMissingOrEmpty('trustedPrLabel', trustedPrLabel);
|
||||||
|
|
||||||
this.githubPullRequests = new GithubPullRequests(githubToken, repoSlug);
|
this.githubPullRequests = new GithubPullRequests(githubToken, repoSlug);
|
||||||
this.githubTeams = new GithubTeams(githubToken, organization);
|
this.githubTeams = new GithubTeams(githubToken, organization);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Methods - Public
|
// Methods - Public
|
||||||
public getPrAuthorTeamMembership(pr: number): Promise<{author: string, isMember: boolean}> {
|
public getPrIsTrusted(pr: number): Promise<boolean> {
|
||||||
return Promise.resolve().
|
return Promise.resolve().
|
||||||
then(() => this.githubPullRequests.fetch(pr)).
|
then(() => this.githubPullRequests.fetch(pr)).
|
||||||
then(prInfo => prInfo.user.login).
|
then(prInfo => this.hasLabel(prInfo, this.trustedPrLabel) ||
|
||||||
then(author => this.githubTeams.isMemberBySlug(author, this.allowedTeamSlugs).
|
this.githubTeams.isMemberBySlug(prInfo.user.login, this.allowedTeamSlugs));
|
||||||
then(isMember => ({author, isMember})));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public verify(expectedPr: number, authHeader: string): Promise<void> {
|
public verify(expectedPr: number, authHeader: string): Promise<BUILD_VERIFICATION_STATUS> {
|
||||||
return Promise.resolve().
|
return Promise.resolve().
|
||||||
then(() => this.extractJwtString(authHeader)).
|
then(() => this.extractJwtString(authHeader)).
|
||||||
then(jwtString => this.verifyJwt(expectedPr, jwtString)).
|
then(jwtString => this.verifyJwt(expectedPr, jwtString)).
|
||||||
@ -52,9 +58,13 @@ export class BuildVerifier {
|
|||||||
return input.replace(/^token +/i, '');
|
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> {
|
protected verifyJwt(expectedPr: number, token: string): Promise<JwtPayload> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
jwt.verify(token, this.secret, {issuer: 'Travis CI, GmbH'}, (err, payload) => {
|
jwt.verify(token, this.secret, {issuer: 'Travis CI, GmbH'}, (err, payload: JwtPayload) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
reject(err.message || err);
|
reject(err.message || err);
|
||||||
} else if (payload.slug !== this.repoSlug) {
|
} else if (payload.slug !== this.repoSlug) {
|
||||||
@ -68,11 +78,10 @@ export class BuildVerifier {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
protected verifyPr(pr: number): Promise<void> {
|
protected verifyPr(pr: number): Promise<BUILD_VERIFICATION_STATUS> {
|
||||||
return this.getPrAuthorTeamMembership(pr).
|
return this.getPrIsTrusted(pr).
|
||||||
then(({author, isMember}) => isMember ? Promise.resolve() : Promise.reject(
|
then(isTrusted => Promise.resolve(isTrusted ?
|
||||||
`User '${author}' is not an active member of any of the following teams: ` +
|
BUILD_VERIFICATION_STATUS.verifiedAndTrusted :
|
||||||
`${this.allowedTeamSlugs.join(', ')}`,
|
BUILD_VERIFICATION_STATUS.verifiedNotTrusted));
|
||||||
));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -12,28 +12,28 @@ function _main() {
|
|||||||
const repoSlug = getEnvVar('AIO_REPO_SLUG');
|
const repoSlug = getEnvVar('AIO_REPO_SLUG');
|
||||||
const organization = getEnvVar('AIO_GITHUB_ORGANIZATION');
|
const organization = getEnvVar('AIO_GITHUB_ORGANIZATION');
|
||||||
const allowedTeamSlugs = getEnvVar('AIO_GITHUB_TEAM_SLUGS').split(',');
|
const allowedTeamSlugs = getEnvVar('AIO_GITHUB_TEAM_SLUGS').split(',');
|
||||||
|
const trustedPrLabel = getEnvVar('AIO_TRUSTED_PR_LABEL');
|
||||||
const pr = +getEnvVar('AIO_PREVERIFY_PR');
|
const pr = +getEnvVar('AIO_PREVERIFY_PR');
|
||||||
|
|
||||||
const buildVerifier = new BuildVerifier(secret, githubToken, repoSlug, organization, allowedTeamSlugs);
|
const buildVerifier = new BuildVerifier(secret, githubToken, repoSlug, organization, allowedTeamSlugs,
|
||||||
|
trustedPrLabel);
|
||||||
|
|
||||||
// Exit codes:
|
// Exit codes:
|
||||||
// - 0: The PR author is a member.
|
// - 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.
|
// - 1: An error occurred.
|
||||||
// - 2: The PR author is not a member.
|
// - 2: The PR cannot be automatically trusted.
|
||||||
buildVerifier.getPrAuthorTeamMembership(pr).
|
buildVerifier.getPrIsTrusted(pr).
|
||||||
then(({author, isMember}) => {
|
then(isTrusted => {
|
||||||
if (isMember) {
|
if (!isTrusted) {
|
||||||
process.exit(0);
|
console.warn(
|
||||||
} else {
|
`The PR cannot be automatically verified, because it doesn't have the "${trustedPrLabel}" label and the ` +
|
||||||
const errorMessage = `User '${author}' is not an active member of any of the following teams: ` +
|
`the author is not an active member of any of the following teams: ${allowedTeamSlugs.join(', ')}`);
|
||||||
`${allowedTeamSlugs.join(', ')}`;
|
|
||||||
onError(errorMessage, 2);
|
|
||||||
}
|
}
|
||||||
}).
|
|
||||||
catch(err => onError(err, 1));
|
|
||||||
}
|
|
||||||
|
|
||||||
function onError(err: string, exitCode: number) {
|
process.exit(isTrusted ? 0 : 2);
|
||||||
console.error(err);
|
}).
|
||||||
process.exit(exitCode || 1);
|
catch(err => {
|
||||||
|
console.error(err);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,24 @@
|
|||||||
// Imports
|
// Imports
|
||||||
import {GithubPullRequests} from '../common/github-pull-requests';
|
import {GithubPullRequests} from '../common/github-pull-requests';
|
||||||
import {BuildVerifier} from './build-verifier';
|
import {BUILD_VERIFICATION_STATUS, BuildVerifier} from './build-verifier';
|
||||||
|
import {UploadError} from './upload-error';
|
||||||
|
|
||||||
// Run
|
// Run
|
||||||
// TODO(gkalpak): Add e2e tests to cover these interactions as well.
|
// TODO(gkalpak): Add e2e tests to cover these interactions as well.
|
||||||
GithubPullRequests.prototype.addComment = () => Promise.resolve();
|
GithubPullRequests.prototype.addComment = () => Promise.resolve();
|
||||||
BuildVerifier.prototype.verify = () => Promise.resolve();
|
BuildVerifier.prototype.verify = (expectedPr: number, authHeader: string) => {
|
||||||
|
switch (authHeader) {
|
||||||
|
case 'FAKE_VERIFICATION_ERROR':
|
||||||
|
// For e2e tests, fake a verification error.
|
||||||
|
return Promise.reject(new UploadError(403, `Error while verifying upload for PR ${expectedPr}: Test`));
|
||||||
|
case 'FAKE_VERIFIED_NOT_TRUSTED':
|
||||||
|
// 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
|
// tslint:disable-next-line: no-var-requires
|
||||||
require('./index');
|
require('./index');
|
||||||
|
@ -10,6 +10,7 @@ const AIO_GITHUB_TEAM_SLUGS = getEnvVar('AIO_GITHUB_TEAM_SLUGS');
|
|||||||
const AIO_GITHUB_TOKEN = getEnvVar('AIO_GITHUB_TOKEN');
|
const AIO_GITHUB_TOKEN = getEnvVar('AIO_GITHUB_TOKEN');
|
||||||
const AIO_PREVIEW_DEPLOYMENT_TOKEN = getEnvVar('AIO_PREVIEW_DEPLOYMENT_TOKEN');
|
const AIO_PREVIEW_DEPLOYMENT_TOKEN = getEnvVar('AIO_PREVIEW_DEPLOYMENT_TOKEN');
|
||||||
const AIO_REPO_SLUG = getEnvVar('AIO_REPO_SLUG');
|
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_HOSTNAME = getEnvVar('AIO_UPLOAD_HOSTNAME');
|
||||||
const AIO_UPLOAD_PORT = +getEnvVar('AIO_UPLOAD_PORT');
|
const AIO_UPLOAD_PORT = +getEnvVar('AIO_UPLOAD_PORT');
|
||||||
const AIO_WWW_USER = getEnvVar('AIO_WWW_USER');
|
const AIO_WWW_USER = getEnvVar('AIO_WWW_USER');
|
||||||
@ -29,6 +30,7 @@ function _main() {
|
|||||||
githubToken: AIO_GITHUB_TOKEN,
|
githubToken: AIO_GITHUB_TOKEN,
|
||||||
repoSlug: AIO_REPO_SLUG,
|
repoSlug: AIO_REPO_SLUG,
|
||||||
secret: AIO_PREVIEW_DEPLOYMENT_TOKEN,
|
secret: AIO_PREVIEW_DEPLOYMENT_TOKEN,
|
||||||
|
trustedPrLabel: AIO_TRUSTED_PR_LABEL,
|
||||||
}).
|
}).
|
||||||
listen(AIO_UPLOAD_PORT, AIO_UPLOAD_HOSTNAME);
|
listen(AIO_UPLOAD_PORT, AIO_UPLOAD_HOSTNAME);
|
||||||
}
|
}
|
||||||
|
@ -4,8 +4,8 @@ import * as http from 'http';
|
|||||||
import {GithubPullRequests} from '../common/github-pull-requests';
|
import {GithubPullRequests} from '../common/github-pull-requests';
|
||||||
import {assertNotMissingOrEmpty} from '../common/utils';
|
import {assertNotMissingOrEmpty} from '../common/utils';
|
||||||
import {BuildCreator} from './build-creator';
|
import {BuildCreator} from './build-creator';
|
||||||
import {CreatedBuildEvent} from './build-events';
|
import {ChangedPrVisibilityEvent, CreatedBuildEvent} from './build-events';
|
||||||
import {BuildVerifier} from './build-verifier';
|
import {BUILD_VERIFICATION_STATUS, BuildVerifier} from './build-verifier';
|
||||||
import {UploadError} from './upload-error';
|
import {UploadError} from './upload-error';
|
||||||
|
|
||||||
// Constants
|
// Constants
|
||||||
@ -21,6 +21,7 @@ interface UploadServerConfig {
|
|||||||
githubToken: string;
|
githubToken: string;
|
||||||
repoSlug: string;
|
repoSlug: string;
|
||||||
secret: string;
|
secret: string;
|
||||||
|
trustedPrLabel: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Classes
|
// Classes
|
||||||
@ -34,14 +35,16 @@ class UploadServerFactory {
|
|||||||
githubToken,
|
githubToken,
|
||||||
repoSlug,
|
repoSlug,
|
||||||
secret,
|
secret,
|
||||||
|
trustedPrLabel,
|
||||||
}: UploadServerConfig): http.Server {
|
}: UploadServerConfig): http.Server {
|
||||||
assertNotMissingOrEmpty('domainName', domainName);
|
assertNotMissingOrEmpty('domainName', domainName);
|
||||||
|
|
||||||
const buildVerifier = new BuildVerifier(secret, githubToken, repoSlug, githubOrganization, githubTeamSlugs);
|
const buildVerifier = new BuildVerifier(secret, githubToken, repoSlug, githubOrganization, githubTeamSlugs,
|
||||||
|
trustedPrLabel);
|
||||||
const buildCreator = this.createBuildCreator(buildsDir, githubToken, repoSlug, domainName);
|
const buildCreator = this.createBuildCreator(buildsDir, githubToken, repoSlug, domainName);
|
||||||
|
|
||||||
const middleware = this.createMiddleware(buildVerifier, buildCreator);
|
const middleware = this.createMiddleware(buildVerifier, buildCreator);
|
||||||
const httpServer = http.createServer(middleware);
|
const httpServer = http.createServer(middleware as any);
|
||||||
|
|
||||||
httpServer.on('listening', () => {
|
httpServer.on('listening', () => {
|
||||||
const info = httpServer.address();
|
const info = httpServer.address();
|
||||||
@ -56,12 +59,24 @@ class UploadServerFactory {
|
|||||||
domainName: string): BuildCreator {
|
domainName: string): BuildCreator {
|
||||||
const buildCreator = new BuildCreator(buildsDir);
|
const buildCreator = new BuildCreator(buildsDir);
|
||||||
const githubPullRequests = new GithubPullRequests(githubToken, repoSlug);
|
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');
|
||||||
|
|
||||||
buildCreator.on(CreatedBuildEvent.type, ({pr, sha}: CreatedBuildEvent) => {
|
return githubPullRequests.addComment(pr, body);
|
||||||
const body = `The angular.io preview for ${sha} is available [here][1].\n\n` +
|
};
|
||||||
`[1]: https://pr${pr}-${sha}.${domainName}/`;
|
|
||||||
|
|
||||||
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;
|
return buildCreator;
|
||||||
@ -80,13 +95,14 @@ class UploadServerFactory {
|
|||||||
this.throwRequestError(401, `Missing or empty '${AUTHORIZATION_HEADER}' header`, req);
|
this.throwRequestError(401, `Missing or empty '${AUTHORIZATION_HEADER}' header`, req);
|
||||||
} else if (!archive) {
|
} else if (!archive) {
|
||||||
this.throwRequestError(400, `Missing or empty '${X_FILE_HEADER}' header`, req);
|
this.throwRequestError(400, `Missing or empty '${X_FILE_HEADER}' header`, req);
|
||||||
|
} else {
|
||||||
|
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));
|
||||||
}
|
}
|
||||||
|
|
||||||
buildVerifier.
|
|
||||||
verify(+pr, authHeader).
|
|
||||||
then(() => buildCreator.create(pr, sha, archive)).
|
|
||||||
then(() => res.sendStatus(201)).
|
|
||||||
catch(err => this.respondWithError(res, err));
|
|
||||||
});
|
});
|
||||||
middleware.get(/^\/health-check\/?$/, (_req, res) => res.sendStatus(200));
|
middleware.get(/^\/health-check\/?$/, (_req, res) => res.sendStatus(200));
|
||||||
middleware.get('*', req => this.throwRequestError(404, 'Unknown resource', req));
|
middleware.get('*', req => this.throwRequestError(404, 'Unknown resource', req));
|
||||||
|
@ -4,6 +4,7 @@ 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 {getEnvVar} from '../common/utils';
|
import {getEnvVar} from '../common/utils';
|
||||||
|
|
||||||
// Constans
|
// Constans
|
||||||
@ -31,10 +32,10 @@ class Helper {
|
|||||||
public get nginxHostname() { return TEST_AIO_NGINX_HOSTNAME; }
|
public get nginxHostname() { return TEST_AIO_NGINX_HOSTNAME; }
|
||||||
public get nginxPortHttp() { return TEST_AIO_NGINX_PORT_HTTP; }
|
public get nginxPortHttp() { return TEST_AIO_NGINX_PORT_HTTP; }
|
||||||
public get nginxPortHttps() { return TEST_AIO_NGINX_PORT_HTTPS; }
|
public get nginxPortHttps() { return TEST_AIO_NGINX_PORT_HTTPS; }
|
||||||
public get wwwUser() { return WWW_USER; }
|
|
||||||
public get uploadHostname() { return TEST_AIO_UPLOAD_HOSTNAME; }
|
public get uploadHostname() { return TEST_AIO_UPLOAD_HOSTNAME; }
|
||||||
public get uploadPort() { return TEST_AIO_UPLOAD_PORT; }
|
public get uploadPort() { return TEST_AIO_UPLOAD_PORT; }
|
||||||
public get uploadMaxSize() { return TEST_AIO_UPLOAD_MAX_SIZE; }
|
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[] = [];
|
||||||
@ -50,6 +51,12 @@ class Helper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Methods - Public
|
// Methods - Public
|
||||||
|
public buildExists(pr: string, sha = '', isPublic = true, legacy = false): boolean {
|
||||||
|
const prDir = this.getPrDir(pr, isPublic);
|
||||||
|
const dir = !sha ? prDir : this.getShaDir(prDir, sha, legacy);
|
||||||
|
return fs.existsSync(dir);
|
||||||
|
}
|
||||||
|
|
||||||
public cleanUp() {
|
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.
|
||||||
@ -62,11 +69,11 @@ class Helper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public createDummyArchive(pr: string, sha: string, archivePath: string): CleanUpFn {
|
public createDummyArchive(pr: string, sha: string, archivePath: string): CleanUpFn {
|
||||||
const inputDir = path.join(this.buildsDir, 'uploaded', pr, sha);
|
const inputDir = this.getShaDir(this.getPrDir(`uploaded/${pr}`, true), sha);
|
||||||
const cmd1 = `tar --create --gzip --directory "${inputDir}" --file "${archivePath}" .`;
|
const cmd1 = `tar --create --gzip --directory "${inputDir}" --file "${archivePath}" .`;
|
||||||
const cmd2 = `chown ${this.wwwUser} ${archivePath}`;
|
const cmd2 = `chown ${this.wwwUser} ${archivePath}`;
|
||||||
|
|
||||||
const cleanUpTemp = this.createDummyBuild(`uploaded/${pr}`, sha, true);
|
const cleanUpTemp = this.createDummyBuild(`uploaded/${pr}`, sha, true, true);
|
||||||
shell.exec(cmd1);
|
shell.exec(cmd1);
|
||||||
shell.exec(cmd2);
|
shell.exec(cmd2);
|
||||||
cleanUpTemp();
|
cleanUpTemp();
|
||||||
@ -74,9 +81,9 @@ class Helper {
|
|||||||
return this.createCleanUpFn(() => shell.rm('-rf', archivePath));
|
return this.createCleanUpFn(() => shell.rm('-rf', archivePath));
|
||||||
}
|
}
|
||||||
|
|
||||||
public createDummyBuild(pr: string, sha: string, force = false): CleanUpFn {
|
public createDummyBuild(pr: string, sha: string, isPublic = true, force = false, legacy = false): CleanUpFn {
|
||||||
const prDir = path.join(this.buildsDir, pr);
|
const prDir = this.getPrDir(pr, isPublic);
|
||||||
const shaDir = path.join(prDir, sha);
|
const shaDir = this.getShaDir(prDir, sha, legacy);
|
||||||
const idxPath = path.join(shaDir, 'index.html');
|
const idxPath = path.join(shaDir, 'index.html');
|
||||||
const barPath = path.join(shaDir, 'foo', 'bar.js');
|
const barPath = path.join(shaDir, 'foo', 'bar.js');
|
||||||
|
|
||||||
@ -87,8 +94,8 @@ class Helper {
|
|||||||
return this.createCleanUpFn(() => shell.rm('-rf', prDir));
|
return this.createCleanUpFn(() => shell.rm('-rf', prDir));
|
||||||
}
|
}
|
||||||
|
|
||||||
public deletePrDir(pr: string) {
|
public deletePrDir(pr: string, isPublic = true) {
|
||||||
const prDir = path.join(this.buildsDir, pr);
|
const prDir = this.getPrDir(pr, isPublic);
|
||||||
|
|
||||||
if (fs.existsSync(prDir)) {
|
if (fs.existsSync(prDir)) {
|
||||||
// Undocumented signature (see https://github.com/shelljs/shelljs/pull/663).
|
// Undocumented signature (see https://github.com/shelljs/shelljs/pull/663).
|
||||||
@ -97,8 +104,22 @@ class Helper {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public readBuildFile(pr: string, sha: string, relFilePath: string): string {
|
public getPrDir(pr: string, isPublic: boolean): string {
|
||||||
const absFilePath = path.join(this.buildsDir, pr, sha, relFilePath);
|
const prDirName = isPublic ? pr : HIDDEN_DIR_PREFIX + pr;
|
||||||
|
return path.join(this.buildsDir, prDirName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public getShaDir(prDir: string, sha: string, legacy = false): string {
|
||||||
|
return path.join(prDir, legacy ? sha : this.getShordSha(sha));
|
||||||
|
}
|
||||||
|
|
||||||
|
public getShordSha(sha: string): 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 absFilePath = path.join(shaDir, relFilePath);
|
||||||
return fs.readFileSync(absFilePath, 'utf8');
|
return fs.readFileSync(absFilePath, 'utf8');
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -129,7 +150,8 @@ class Helper {
|
|||||||
const [headers, body] = result.stdout.
|
const [headers, body] = result.stdout.
|
||||||
split(/(?:\r?\n){2,}/).
|
split(/(?:\r?\n){2,}/).
|
||||||
map(s => s.trim()).
|
map(s => s.trim()).
|
||||||
slice(-2);
|
slice(-2); // In case of redirect, discard the previous headers.
|
||||||
|
// Only keep the last to sections (final headers and body).
|
||||||
|
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
console.log('Stdout:', result.stdout);
|
console.log('Stdout:', result.stdout);
|
||||||
@ -143,8 +165,10 @@ class Helper {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public writeBuildFile(pr: string, sha: string, relFilePath: string, content: string): CleanUpFn {
|
public writeBuildFile(pr: string, sha: string, relFilePath: string, content: string, isPublic = true,
|
||||||
const absFilePath = path.join(this.buildsDir, pr, sha, relFilePath);
|
legacy = false): CleanUpFn {
|
||||||
|
const shaDir = this.getShaDir(this.getPrDir(pr, isPublic), sha, legacy);
|
||||||
|
const absFilePath = path.join(shaDir, relFilePath);
|
||||||
return this.writeFile(absFilePath, {content}, true);
|
return this.writeFile(absFilePath, {content}, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -31,111 +31,184 @@ describe(`nginx`, () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
h.runForAllSupportedSchemes((scheme, port) => describe(`nginx (on ${scheme.toUpperCase()})`, () => {
|
h.runForAllSupportedSchemes((scheme, port) => describe(`(on ${scheme.toUpperCase()})`, () => {
|
||||||
const hostname = h.nginxHostname;
|
const hostname = h.nginxHostname;
|
||||||
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 shortSha0 = h.getShordSha(sha0);
|
||||||
|
|
||||||
|
|
||||||
describe(`pr<pr>-<sha>.${host}/*`, () => {
|
describe(`pr<pr>-<sha>.${host}/*`, () => {
|
||||||
|
|
||||||
beforeEach(() => {
|
describe('(for public builds)', () => {
|
||||||
h.createDummyBuild(pr, sha9);
|
|
||||||
h.createDummyBuild(pr, sha0);
|
beforeEach(() => {
|
||||||
|
h.createDummyBuild(pr, sha9);
|
||||||
|
h.createDummyBuild(pr, sha0);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should return /index.html', done => {
|
||||||
|
const origin = `${scheme}://pr${pr}-${shortSha9}.${host}`;
|
||||||
|
const bodyRegex = new RegExp(`^PR: ${pr} | SHA: ${sha9} | File: /index\\.html$`);
|
||||||
|
|
||||||
|
Promise.all([
|
||||||
|
h.runCmd(`curl -iL ${origin}/index.html`).then(h.verifyResponse(200, bodyRegex)),
|
||||||
|
h.runCmd(`curl -iL ${origin}/`).then(h.verifyResponse(200, bodyRegex)),
|
||||||
|
h.runCmd(`curl -iL ${origin}`).then(h.verifyResponse(200, bodyRegex)),
|
||||||
|
]).then(done);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should return /index.html (for legacy builds)', done => {
|
||||||
|
const origin = `${scheme}://pr${pr}-${sha9}.${host}`;
|
||||||
|
const bodyRegex = new RegExp(`^PR: ${pr} | SHA: ${sha9} | File: /index\\.html$`);
|
||||||
|
|
||||||
|
h.createDummyBuild(pr, sha9, true, false, true);
|
||||||
|
|
||||||
|
Promise.all([
|
||||||
|
h.runCmd(`curl -iL ${origin}/index.html`).then(h.verifyResponse(200, bodyRegex)),
|
||||||
|
h.runCmd(`curl -iL ${origin}/`).then(h.verifyResponse(200, bodyRegex)),
|
||||||
|
h.runCmd(`curl -iL ${origin}`).then(h.verifyResponse(200, bodyRegex)),
|
||||||
|
]).then(done);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should return /foo/bar.js', done => {
|
||||||
|
const bodyRegex = new RegExp(`^PR: ${pr} | SHA: ${sha9} | File: /foo/bar\\.js$`);
|
||||||
|
|
||||||
|
h.runCmd(`curl -iL ${scheme}://pr${pr}-${shortSha9}.${host}/foo/bar.js`).
|
||||||
|
then(h.verifyResponse(200, bodyRegex)).
|
||||||
|
then(done);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should return /foo/bar.js (for legacy builds)', done => {
|
||||||
|
const origin = `${scheme}://pr${pr}-${sha9}.${host}`;
|
||||||
|
const bodyRegex = new RegExp(`^PR: ${pr} | SHA: ${sha9} | File: /foo/bar\\.js$`);
|
||||||
|
|
||||||
|
h.createDummyBuild(pr, sha9, true, false, true);
|
||||||
|
|
||||||
|
h.runCmd(`curl -iL ${origin}/foo/bar.js`).
|
||||||
|
then(h.verifyResponse(200, bodyRegex)).
|
||||||
|
then(done);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should respond with 403 for directories', done => {
|
||||||
|
Promise.all([
|
||||||
|
h.runCmd(`curl -iL ${scheme}://pr${pr}-${shortSha9}.${host}/foo/`).then(h.verifyResponse(403)),
|
||||||
|
h.runCmd(`curl -iL ${scheme}://pr${pr}-${shortSha9}.${host}/foo`).then(h.verifyResponse(403)),
|
||||||
|
]).then(done);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should respond with 404 for unknown paths to files', done => {
|
||||||
|
h.runCmd(`curl -iL ${scheme}://pr${pr}-${shortSha9}.${host}/foo/baz.css`).
|
||||||
|
then(h.verifyResponse(404)).
|
||||||
|
then(done);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should rewrite to \'index.html\' for unknown paths that don\'t look like files', done => {
|
||||||
|
const origin = `${scheme}://pr${pr}-${shortSha9}.${host}`;
|
||||||
|
const bodyRegex = new RegExp(`^PR: ${pr} | SHA: ${sha9} | File: /index\\.html$`);
|
||||||
|
|
||||||
|
Promise.all([
|
||||||
|
h.runCmd(`curl -iL ${origin}/foo/baz`).then(h.verifyResponse(200, bodyRegex)),
|
||||||
|
h.runCmd(`curl -iL ${origin}/foo/baz/`).then(h.verifyResponse(200, bodyRegex)),
|
||||||
|
]).then(done);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should respond with 404 for unknown PRs/SHAs', done => {
|
||||||
|
const otherPr = 54321;
|
||||||
|
const otherShortSha = h.getShordSha('8'.repeat(40));
|
||||||
|
|
||||||
|
Promise.all([
|
||||||
|
h.runCmd(`curl -iL ${scheme}://pr${pr}9-${shortSha9}.${host}`).then(h.verifyResponse(404)),
|
||||||
|
h.runCmd(`curl -iL ${scheme}://pr${otherPr}-${shortSha9}.${host}`).then(h.verifyResponse(404)),
|
||||||
|
h.runCmd(`curl -iL ${scheme}://pr${pr}-${shortSha9}9.${host}`).then(h.verifyResponse(404)),
|
||||||
|
h.runCmd(`curl -iL ${scheme}://pr${pr}-${otherShortSha}.${host}`).then(h.verifyResponse(404)),
|
||||||
|
]).then(done);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should respond with 404 if the subdomain format is wrong', done => {
|
||||||
|
Promise.all([
|
||||||
|
h.runCmd(`curl -iL ${scheme}://xpr${pr}-${shortSha9}.${host}`).then(h.verifyResponse(404)),
|
||||||
|
h.runCmd(`curl -iL ${scheme}://prx${pr}-${shortSha9}.${host}`).then(h.verifyResponse(404)),
|
||||||
|
h.runCmd(`curl -iL ${scheme}://xx${pr}-${shortSha9}.${host}`).then(h.verifyResponse(404)),
|
||||||
|
h.runCmd(`curl -iL ${scheme}://p${pr}-${shortSha9}.${host}`).then(h.verifyResponse(404)),
|
||||||
|
h.runCmd(`curl -iL ${scheme}://r${pr}-${shortSha9}.${host}`).then(h.verifyResponse(404)),
|
||||||
|
h.runCmd(`curl -iL ${scheme}://${pr}-${shortSha9}.${host}`).then(h.verifyResponse(404)),
|
||||||
|
h.runCmd(`curl -iL ${scheme}://pr${pr}${shortSha9}.${host}`).then(h.verifyResponse(404)),
|
||||||
|
h.runCmd(`curl -iL ${scheme}://pr${pr}_${shortSha9}.${host}`).then(h.verifyResponse(404)),
|
||||||
|
]).then(done);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should reject PRs with leading zeros', done => {
|
||||||
|
h.runCmd(`curl -iL ${scheme}://pr0${pr}-${shortSha9}.${host}`).
|
||||||
|
then(h.verifyResponse(404)).
|
||||||
|
then(done);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should accept SHAs with leading zeros (but not trim the zeros)', done => {
|
||||||
|
const bodyRegex9 = new RegExp(`^PR: ${pr} | SHA: ${sha9} | File: /index\\.html$`);
|
||||||
|
const bodyRegex0 = new RegExp(`^PR: ${pr} | SHA: ${sha0} | File: /index\\.html$`);
|
||||||
|
|
||||||
|
Promise.all([
|
||||||
|
h.runCmd(`curl -iL ${scheme}://pr${pr}-0${shortSha9}.${host}`).then(h.verifyResponse(404)),
|
||||||
|
h.runCmd(`curl -iL ${scheme}://pr${pr}-${shortSha9}.${host}`).then(h.verifyResponse(200, bodyRegex9)),
|
||||||
|
h.runCmd(`curl -iL ${scheme}://pr${pr}-${shortSha0}.${host}`).then(h.verifyResponse(200, bodyRegex0)),
|
||||||
|
]).then(done);
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
it('should return /index.html', done => {
|
describe('(for hidden builds)', () => {
|
||||||
const origin = `${scheme}://pr${pr}-${sha9}.${host}`;
|
|
||||||
const bodyRegex = new RegExp(`^PR: ${pr} | SHA: ${sha9} | File: /index\\.html$`);
|
|
||||||
|
|
||||||
Promise.all([
|
it('should respond with 404 for any file or directory', done => {
|
||||||
h.runCmd(`curl -iL ${origin}/index.html`).then(h.verifyResponse(200, bodyRegex)),
|
const origin = `${scheme}://pr${pr}-${shortSha9}.${host}`;
|
||||||
h.runCmd(`curl -iL ${origin}/`).then(h.verifyResponse(200, bodyRegex)),
|
const assert404 = h.verifyResponse(404);
|
||||||
h.runCmd(`curl -iL ${origin}`).then(h.verifyResponse(200, bodyRegex)),
|
|
||||||
]).then(done);
|
h.createDummyBuild(pr, sha9, false);
|
||||||
});
|
expect(h.buildExists(pr, sha9, false)).toBe(true);
|
||||||
|
|
||||||
|
Promise.all([
|
||||||
|
h.runCmd(`curl -iL ${origin}/index.html`).then(assert404),
|
||||||
|
h.runCmd(`curl -iL ${origin}/`).then(assert404),
|
||||||
|
h.runCmd(`curl -iL ${origin}`).then(assert404),
|
||||||
|
h.runCmd(`curl -iL ${origin}/foo/bar.js`).then(assert404),
|
||||||
|
h.runCmd(`curl -iL ${origin}/foo/`).then(assert404),
|
||||||
|
h.runCmd(`curl -iL ${origin}/foo`).then(assert404),
|
||||||
|
]).then(done);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
it('should return /foo/bar.js', done => {
|
it('should respond with 404 for any file or directory (for legacy builds)', done => {
|
||||||
const bodyRegex = new RegExp(`^PR: ${pr} | SHA: ${sha9} | File: /foo/bar\\.js$`);
|
const origin = `${scheme}://pr${pr}-${sha9}.${host}`;
|
||||||
|
const assert404 = h.verifyResponse(404);
|
||||||
|
|
||||||
h.runCmd(`curl -iL ${scheme}://pr${pr}-${sha9}.${host}/foo/bar.js`).
|
h.createDummyBuild(pr, sha9, false, false, true);
|
||||||
then(h.verifyResponse(200, bodyRegex)).
|
expect(h.buildExists(pr, sha9, false, true)).toBe(true);
|
||||||
then(done);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
Promise.all([
|
||||||
|
h.runCmd(`curl -iL ${origin}/index.html`).then(assert404),
|
||||||
|
h.runCmd(`curl -iL ${origin}/`).then(assert404),
|
||||||
|
h.runCmd(`curl -iL ${origin}`).then(assert404),
|
||||||
|
h.runCmd(`curl -iL ${origin}/foo/bar.js`).then(assert404),
|
||||||
|
h.runCmd(`curl -iL ${origin}/foo/`).then(assert404),
|
||||||
|
h.runCmd(`curl -iL ${origin}/foo`).then(assert404),
|
||||||
|
]).then(done);
|
||||||
|
});
|
||||||
|
|
||||||
it('should respond with 403 for directories', done => {
|
|
||||||
Promise.all([
|
|
||||||
h.runCmd(`curl -iL ${scheme}://pr${pr}-${sha9}.${host}/foo/`).then(h.verifyResponse(403)),
|
|
||||||
h.runCmd(`curl -iL ${scheme}://pr${pr}-${sha9}.${host}/foo`).then(h.verifyResponse(403)),
|
|
||||||
]).then(done);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
it('should respond with 404 for unknown paths to files', done => {
|
|
||||||
h.runCmd(`curl -iL ${scheme}://pr${pr}-${sha9}.${host}/foo/baz.css`).
|
|
||||||
then(h.verifyResponse(404)).
|
|
||||||
then(done);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
it('should rewrite to \'index.html\' for unknown paths that don\'t look like files', done => {
|
|
||||||
const bodyRegex = new RegExp(`^PR: ${pr} | SHA: ${sha9} | File: /index\\.html$`);
|
|
||||||
|
|
||||||
Promise.all([
|
|
||||||
h.runCmd(`curl -iL ${scheme}://pr${pr}-${sha9}.${host}/foo/baz`).then(h.verifyResponse(200, bodyRegex)),
|
|
||||||
h.runCmd(`curl -iL ${scheme}://pr${pr}-${sha9}.${host}/foo/baz/`).then(h.verifyResponse(200, bodyRegex)),
|
|
||||||
]).then(done);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
it('should respond with 404 for unknown PRs/SHAs', done => {
|
|
||||||
const otherPr = 54321;
|
|
||||||
const otherSha = '8'.repeat(40);
|
|
||||||
|
|
||||||
Promise.all([
|
|
||||||
h.runCmd(`curl -iL ${scheme}://pr${pr}9-${sha9}.${host}`).then(h.verifyResponse(404)),
|
|
||||||
h.runCmd(`curl -iL ${scheme}://pr${otherPr}-${sha9}.${host}`).then(h.verifyResponse(404)),
|
|
||||||
h.runCmd(`curl -iL ${scheme}://pr${pr}-${sha9}9.${host}`).then(h.verifyResponse(404)),
|
|
||||||
h.runCmd(`curl -iL ${scheme}://pr${pr}-${otherSha}.${host}`).then(h.verifyResponse(404)),
|
|
||||||
]).then(done);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
it('should respond with 404 if the subdomain format is wrong', done => {
|
|
||||||
Promise.all([
|
|
||||||
h.runCmd(`curl -iL ${scheme}://xpr${pr}-${sha9}.${host}`).then(h.verifyResponse(404)),
|
|
||||||
h.runCmd(`curl -iL ${scheme}://prx${pr}-${sha9}.${host}`).then(h.verifyResponse(404)),
|
|
||||||
h.runCmd(`curl -iL ${scheme}://xx${pr}-${sha9}.${host}`).then(h.verifyResponse(404)),
|
|
||||||
h.runCmd(`curl -iL ${scheme}://p${pr}-${sha9}.${host}`).then(h.verifyResponse(404)),
|
|
||||||
h.runCmd(`curl -iL ${scheme}://r${pr}-${sha9}.${host}`).then(h.verifyResponse(404)),
|
|
||||||
h.runCmd(`curl -iL ${scheme}://${pr}-${sha9}.${host}`).then(h.verifyResponse(404)),
|
|
||||||
h.runCmd(`curl -iL ${scheme}://pr${pr}${sha9}.${host}`).then(h.verifyResponse(404)),
|
|
||||||
h.runCmd(`curl -iL ${scheme}://pr${pr}_${sha9}.${host}`).then(h.verifyResponse(404)),
|
|
||||||
]).then(done);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
it('should reject PRs with leading zeros', done => {
|
|
||||||
h.runCmd(`curl -iL ${scheme}://pr0${pr}-${sha9}.${host}`).
|
|
||||||
then(h.verifyResponse(404)).
|
|
||||||
then(done);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
it('should accept SHAs with leading zeros (but not trim the zeros)', done => {
|
|
||||||
const bodyRegex9 = new RegExp(`^PR: ${pr} | SHA: ${sha9} | File: /index\\.html$`);
|
|
||||||
const bodyRegex0 = new RegExp(`^PR: ${pr} | SHA: ${sha0} | File: /index\\.html$`);
|
|
||||||
|
|
||||||
Promise.all([
|
|
||||||
h.runCmd(`curl -iL ${scheme}://pr${pr}-0${sha9}.${host}`).then(h.verifyResponse(404)),
|
|
||||||
h.runCmd(`curl -iL ${scheme}://pr${pr}-${sha9}.${host}`).then(h.verifyResponse(200, bodyRegex9)),
|
|
||||||
h.runCmd(`curl -iL ${scheme}://pr${pr}-${sha0}.${host}`).then(h.verifyResponse(200, bodyRegex0)),
|
|
||||||
]).then(done);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
@ -246,7 +319,7 @@ describe(`nginx`, () => {
|
|||||||
|
|
||||||
describe(`${host}/*`, () => {
|
describe(`${host}/*`, () => {
|
||||||
|
|
||||||
it('should respond with 404 for unkown URLs (even if the resource exists)', done => {
|
it('should respond with 404 for unknown URLs (even if the resource exists)', done => {
|
||||||
['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(h.buildsDir, relFilePath);
|
||||||
h.writeFile(absFilePath, {content: `File: /${relFilePath}`});
|
h.writeFile(absFilePath, {content: `File: /${relFilePath}`});
|
||||||
|
@ -12,73 +12,189 @@ h.runForAllSupportedSchemes((scheme, port) => describe(`integration (on ${scheme
|
|||||||
const archivePath = path.join(h.buildsDir, 'snapshot.tar.gz');
|
const archivePath = path.join(h.buildsDir, 'snapshot.tar.gz');
|
||||||
|
|
||||||
const getFile = (pr: string, sha: string, file: string) =>
|
const getFile = (pr: string, sha: string, file: string) =>
|
||||||
h.runCmd(`curl -iL ${scheme}://pr${pr}-${sha}.${host}/${file}`);
|
h.runCmd(`curl -iL ${scheme}://pr${pr}-${h.getShordSha(sha)}.${host}/${file}`);
|
||||||
const uploadBuild = (pr: string, sha: string, archive: string) => {
|
const uploadBuild = (pr: string, sha: string, archive: string, authHeader = 'Token FOO') => {
|
||||||
const curlPost = 'curl -iLX POST --header "Authorization: Token FOO"';
|
// Using `FAKE_VERIFICATION_ERROR` or `FAKE_VERIFIED_NOT_TRUSTED` as `authHeader`,
|
||||||
|
// we can fake the response of the overwritten `BuildVerifier.verify()` method.
|
||||||
|
// (See 'lib/upload-server/index-test.ts'.)
|
||||||
|
const curlPost = `curl -iLX POST --header "Authorization: ${authHeader}"`;
|
||||||
return h.runCmd(`${curlPost} --data-binary "@${archive}" ${scheme}://${host}/create-build/${pr}/${sha}`);
|
return h.runCmd(`${curlPost} --data-binary "@${archive}" ${scheme}://${host}/create-build/${pr}/${sha}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(() => jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000);
|
beforeEach(() => jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000);
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
h.deletePrDir(pr9);
|
h.deletePrDir(pr9);
|
||||||
|
h.deletePrDir(pr9, false);
|
||||||
h.cleanUp();
|
h.cleanUp();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
it('should be able to upload and serve a build for a new PR', done => {
|
describe('for a new PR', () => {
|
||||||
const regexPrefix9 = `^PR: uploaded\\/${pr9} \\| SHA: ${sha9} \\| File:`;
|
|
||||||
const idxContentRegex9 = new RegExp(`${regexPrefix9} \\/index\\.html$`);
|
|
||||||
const barContentRegex9 = new RegExp(`${regexPrefix9} \\/foo\\/bar\\.js$`);
|
|
||||||
|
|
||||||
h.createDummyArchive(pr9, sha9, archivePath);
|
it('should be able to upload and serve a public build', done => {
|
||||||
|
const regexPrefix9 = `^PR: uploaded\\/${pr9} \\| SHA: ${sha9} \\| File:`;
|
||||||
|
const idxContentRegex9 = new RegExp(`${regexPrefix9} \\/index\\.html$`);
|
||||||
|
const barContentRegex9 = new RegExp(`${regexPrefix9} \\/foo\\/bar\\.js$`);
|
||||||
|
|
||||||
|
h.createDummyArchive(pr9, sha9, archivePath);
|
||||||
|
|
||||||
|
uploadBuild(pr9, sha9, archivePath).
|
||||||
|
then(() => Promise.all([
|
||||||
|
getFile(pr9, sha9, 'index.html').then(h.verifyResponse(200, idxContentRegex9)),
|
||||||
|
getFile(pr9, sha9, 'foo/bar.js').then(h.verifyResponse(200, barContentRegex9)),
|
||||||
|
])).
|
||||||
|
then(done);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should be able to upload but not serve a hidden build', done => {
|
||||||
|
const regexPrefix9 = `^PR: uploaded\\/${pr9} \\| SHA: ${sha9} \\| File:`;
|
||||||
|
const idxContentRegex9 = new RegExp(`${regexPrefix9} \\/index\\.html$`);
|
||||||
|
const barContentRegex9 = new RegExp(`${regexPrefix9} \\/foo\\/bar\\.js$`);
|
||||||
|
|
||||||
|
h.createDummyArchive(pr9, sha9, archivePath);
|
||||||
|
|
||||||
|
uploadBuild(pr9, sha9, archivePath, 'FAKE_VERIFIED_NOT_TRUSTED').
|
||||||
|
then(() => Promise.all([
|
||||||
|
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 => {
|
||||||
|
const errorRegex9 = new RegExp(`Error while verifying upload for PR ${pr9}: Test`);
|
||||||
|
|
||||||
|
h.createDummyArchive(pr9, sha9, archivePath);
|
||||||
|
|
||||||
|
uploadBuild(pr9, sha9, archivePath, 'FAKE_VERIFICATION_ERROR').
|
||||||
|
then(h.verifyResponse(403, errorRegex9)).
|
||||||
|
then(() => {
|
||||||
|
expect(h.buildExists(pr9)).toBe(false);
|
||||||
|
expect(h.buildExists(pr9, '', false)).toBe(false);
|
||||||
|
}).
|
||||||
|
then(done);
|
||||||
|
});
|
||||||
|
|
||||||
uploadBuild(pr9, sha9, archivePath).
|
|
||||||
then(() => Promise.all([
|
|
||||||
getFile(pr9, sha9, 'index.html').then(h.verifyResponse(200, idxContentRegex9)),
|
|
||||||
getFile(pr9, sha9, 'foo/bar.js').then(h.verifyResponse(200, barContentRegex9)),
|
|
||||||
])).
|
|
||||||
then(done);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
it('should be able to upload and serve a build for an existing PR', done => {
|
describe('for an existing PR', () => {
|
||||||
const regexPrefix0 = `^PR: ${pr9} \\| SHA: ${sha0} \\| File:`;
|
|
||||||
const idxContentRegex0 = new RegExp(`${regexPrefix0} \\/index\\.html$`);
|
|
||||||
const barContentRegex0 = new RegExp(`${regexPrefix0} \\/foo\\/bar\\.js$`);
|
|
||||||
|
|
||||||
const regexPrefix9 = `^PR: uploaded\\/${pr9} \\| SHA: ${sha9} \\| File:`;
|
it('should be able to upload and serve a public build', done => {
|
||||||
const idxContentRegex9 = new RegExp(`${regexPrefix9} \\/index\\.html$`);
|
const regexPrefix0 = `^PR: ${pr9} \\| SHA: ${sha0} \\| File:`;
|
||||||
const barContentRegex9 = new RegExp(`${regexPrefix9} \\/foo\\/bar\\.js$`);
|
const idxContentRegex0 = new RegExp(`${regexPrefix0} \\/index\\.html$`);
|
||||||
|
const barContentRegex0 = new RegExp(`${regexPrefix0} \\/foo\\/bar\\.js$`);
|
||||||
|
|
||||||
h.createDummyBuild(pr9, sha0);
|
const regexPrefix9 = `^PR: uploaded\\/${pr9} \\| SHA: ${sha9} \\| File:`;
|
||||||
h.createDummyArchive(pr9, sha9, archivePath);
|
const idxContentRegex9 = new RegExp(`${regexPrefix9} \\/index\\.html$`);
|
||||||
|
const barContentRegex9 = new RegExp(`${regexPrefix9} \\/foo\\/bar\\.js$`);
|
||||||
|
|
||||||
uploadBuild(pr9, sha9, archivePath).
|
h.createDummyBuild(pr9, sha0);
|
||||||
then(() => Promise.all([
|
h.createDummyArchive(pr9, sha9, archivePath);
|
||||||
getFile(pr9, sha0, 'index.html').then(h.verifyResponse(200, idxContentRegex0)),
|
|
||||||
getFile(pr9, sha0, 'foo/bar.js').then(h.verifyResponse(200, barContentRegex0)),
|
uploadBuild(pr9, sha9, archivePath).
|
||||||
getFile(pr9, sha9, 'index.html').then(h.verifyResponse(200, idxContentRegex9)),
|
then(() => Promise.all([
|
||||||
getFile(pr9, sha9, 'foo/bar.js').then(h.verifyResponse(200, barContentRegex9)),
|
getFile(pr9, sha0, 'index.html').then(h.verifyResponse(200, idxContentRegex0)),
|
||||||
])).
|
getFile(pr9, sha0, 'foo/bar.js').then(h.verifyResponse(200, barContentRegex0)),
|
||||||
then(done);
|
getFile(pr9, sha9, 'index.html').then(h.verifyResponse(200, idxContentRegex9)),
|
||||||
});
|
getFile(pr9, sha9, 'foo/bar.js').then(h.verifyResponse(200, barContentRegex9)),
|
||||||
|
])).
|
||||||
|
then(done);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
it('should not be able to overwrite a build', done => {
|
it('should be able to upload but not serve a hidden build', done => {
|
||||||
const regexPrefix9 = `^PR: ${pr9} \\| SHA: ${sha9} \\| File:`;
|
const regexPrefix0 = `^PR: ${pr9} \\| SHA: ${sha0} \\| File:`;
|
||||||
const idxContentRegex9 = new RegExp(`${regexPrefix9} \\/index\\.html$`);
|
const idxContentRegex0 = new RegExp(`${regexPrefix0} \\/index\\.html$`);
|
||||||
const barContentRegex9 = new RegExp(`${regexPrefix9} \\/foo\\/bar\\.js$`);
|
const barContentRegex0 = new RegExp(`${regexPrefix0} \\/foo\\/bar\\.js$`);
|
||||||
|
|
||||||
h.createDummyBuild(pr9, sha9);
|
const regexPrefix9 = `^PR: uploaded\\/${pr9} \\| SHA: ${sha9} \\| File:`;
|
||||||
h.createDummyArchive(pr9, sha9, archivePath);
|
const idxContentRegex9 = new RegExp(`${regexPrefix9} \\/index\\.html$`);
|
||||||
|
const barContentRegex9 = new RegExp(`${regexPrefix9} \\/foo\\/bar\\.js$`);
|
||||||
|
|
||||||
|
h.createDummyBuild(pr9, sha0, false);
|
||||||
|
h.createDummyArchive(pr9, sha9, archivePath);
|
||||||
|
|
||||||
|
uploadBuild(pr9, sha9, archivePath, 'FAKE_VERIFIED_NOT_TRUSTED').
|
||||||
|
then(() => Promise.all([
|
||||||
|
getFile(pr9, sha0, 'index.html').then(h.verifyResponse(404)),
|
||||||
|
getFile(pr9, sha0, 'foo/bar.js').then(h.verifyResponse(404)),
|
||||||
|
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 => {
|
||||||
|
const errorRegex9 = new RegExp(`Error while verifying upload for PR ${pr9}: Test`);
|
||||||
|
|
||||||
|
h.createDummyBuild(pr9, sha0);
|
||||||
|
h.createDummyArchive(pr9, sha9, archivePath);
|
||||||
|
|
||||||
|
uploadBuild(pr9, sha9, archivePath, 'FAKE_VERIFICATION_ERROR').
|
||||||
|
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);
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should not be able to overwrite an existing public build', done => {
|
||||||
|
const regexPrefix9 = `^PR: ${pr9} \\| SHA: ${sha9} \\| File:`;
|
||||||
|
const idxContentRegex9 = new RegExp(`${regexPrefix9} \\/index\\.html$`);
|
||||||
|
const barContentRegex9 = new RegExp(`${regexPrefix9} \\/foo\\/bar\\.js$`);
|
||||||
|
|
||||||
|
h.createDummyBuild(pr9, sha9);
|
||||||
|
h.createDummyArchive(pr9, sha9, archivePath);
|
||||||
|
|
||||||
|
uploadBuild(pr9, sha9, archivePath).
|
||||||
|
then(h.verifyResponse(409)).
|
||||||
|
then(() => Promise.all([
|
||||||
|
getFile(pr9, sha9, 'index.html').then(h.verifyResponse(200, idxContentRegex9)),
|
||||||
|
getFile(pr9, sha9, 'foo/bar.js').then(h.verifyResponse(200, barContentRegex9)),
|
||||||
|
])).
|
||||||
|
then(done);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should not be able to overwrite an existing hidden build', done => {
|
||||||
|
const regexPrefix9 = `^PR: ${pr9} \\| SHA: ${sha9} \\| File:`;
|
||||||
|
const idxContentRegex9 = new RegExp(`${regexPrefix9} \\/index\\.html$`);
|
||||||
|
const barContentRegex9 = new RegExp(`${regexPrefix9} \\/foo\\/bar\\.js$`);
|
||||||
|
|
||||||
|
h.createDummyBuild(pr9, sha9, false);
|
||||||
|
h.createDummyArchive(pr9, sha9, archivePath);
|
||||||
|
|
||||||
|
uploadBuild(pr9, sha9, archivePath, 'FAKE_VERIFIED_NOT_TRUSTED').
|
||||||
|
then(h.verifyResponse(409)).
|
||||||
|
then(() => {
|
||||||
|
expect(h.readBuildFile(pr9, sha9, 'index.html', false)).toMatch(idxContentRegex9);
|
||||||
|
expect(h.readBuildFile(pr9, sha9, 'foo/bar.js', false)).toMatch(barContentRegex9);
|
||||||
|
}).
|
||||||
|
then(done);
|
||||||
|
});
|
||||||
|
|
||||||
uploadBuild(pr9, sha9, archivePath).
|
|
||||||
then(h.verifyResponse(409)).
|
|
||||||
then(() => Promise.all([
|
|
||||||
getFile(pr9, sha9, 'index.html').then(h.verifyResponse(200, idxContentRegex9)),
|
|
||||||
getFile(pr9, sha9, 'foo/bar.js').then(h.verifyResponse(200, barContentRegex9)),
|
|
||||||
])).
|
|
||||||
then(done);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
}));
|
}));
|
||||||
|
@ -19,7 +19,8 @@ describe('upload-server (on HTTP)', () => {
|
|||||||
describe(`${host}/create-build/<pr>/<sha>`, () => {
|
describe(`${host}/create-build/<pr>/<sha>`, () => {
|
||||||
const authorizationHeader = `--header "Authorization: Token FOO"`;
|
const authorizationHeader = `--header "Authorization: Token FOO"`;
|
||||||
const xFileHeader = `--header "X-File: ${h.buildsDir}/snapshot.tar.gz"`;
|
const xFileHeader = `--header "X-File: ${h.buildsDir}/snapshot.tar.gz"`;
|
||||||
const curl = `curl -iL ${authorizationHeader} ${xFileHeader}`;
|
const defaultHeaders = `${authorizationHeader} ${xFileHeader}`;
|
||||||
|
const curl = (url: string, headers = defaultHeaders) => `curl -iL ${headers} ${url}`;
|
||||||
|
|
||||||
|
|
||||||
it('should disallow non-GET requests', done => {
|
it('should disallow non-GET requests', done => {
|
||||||
@ -42,8 +43,8 @@ describe('upload-server (on HTTP)', () => {
|
|||||||
const bodyRegex = /^Missing or empty 'AUTHORIZATION' header/;
|
const bodyRegex = /^Missing or empty 'AUTHORIZATION' header/;
|
||||||
|
|
||||||
Promise.all([
|
Promise.all([
|
||||||
h.runCmd(`curl -iL ${headers1} ${url}`).then(h.verifyResponse(401, bodyRegex)),
|
h.runCmd(curl(url, headers1)).then(h.verifyResponse(401, bodyRegex)),
|
||||||
h.runCmd(`curl -iL ${headers2} ${url}`).then(h.verifyResponse(401, bodyRegex)),
|
h.runCmd(curl(url, headers2)).then(h.verifyResponse(401, bodyRegex)),
|
||||||
]).then(done);
|
]).then(done);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -55,14 +56,25 @@ describe('upload-server (on HTTP)', () => {
|
|||||||
const bodyRegex = /^Missing or empty 'X-FILE' header/;
|
const bodyRegex = /^Missing or empty 'X-FILE' header/;
|
||||||
|
|
||||||
Promise.all([
|
Promise.all([
|
||||||
h.runCmd(`curl -iL ${headers1} ${url}`).then(h.verifyResponse(400, bodyRegex)),
|
h.runCmd(curl(url, headers1)).then(h.verifyResponse(400, bodyRegex)),
|
||||||
h.runCmd(`curl -iL ${headers2} ${url}`).then(h.verifyResponse(400, bodyRegex)),
|
h.runCmd(curl(url, headers2)).then(h.verifyResponse(400, bodyRegex)),
|
||||||
]).then(done);
|
]).then(done);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should reject requests for which the PR verification fails', done => {
|
||||||
|
const headers = `--header "Authorization: FAKE_VERIFICATION_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 => {
|
it('should respond with 404 for unknown paths', done => {
|
||||||
const cmdPrefix = `${curl} http://${host}`;
|
const cmdPrefix = curl(`http://${host}`);
|
||||||
|
|
||||||
Promise.all([
|
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)),
|
||||||
@ -78,7 +90,7 @@ describe('upload-server (on HTTP)', () => {
|
|||||||
|
|
||||||
|
|
||||||
it('should reject PRs with leading zeros', done => {
|
it('should reject PRs with leading zeros', done => {
|
||||||
h.runCmd(`${curl} http://${host}/create-build/0${pr}/${sha9}`).
|
h.runCmd(curl(`http://${host}/create-build/0${pr}/${sha9}`)).
|
||||||
then(h.verifyResponse(404)).
|
then(h.verifyResponse(404)).
|
||||||
then(done);
|
then(done);
|
||||||
});
|
});
|
||||||
@ -86,129 +98,253 @@ describe('upload-server (on HTTP)', () => {
|
|||||||
|
|
||||||
it('should accept SHAs with leading zeros (but not trim the zeros)', done => {
|
it('should accept SHAs with leading zeros (but not trim the zeros)', done => {
|
||||||
Promise.all([
|
Promise.all([
|
||||||
h.runCmd(`${curl} http://${host}/create-build/${pr}/0${sha9}`).then(h.verifyResponse(404)),
|
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}/${sha9}`)).then(h.verifyResponse(500)),
|
||||||
h.runCmd(`${curl} http://${host}/create-build/${pr}/${sha0}`).then(h.verifyResponse(500)),
|
h.runCmd(curl(`http://${host}/create-build/${pr}/${sha0}`)).then(h.verifyResponse(500)),
|
||||||
]).then(done);
|
]).then(done);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
it('should not overwrite existing builds', done => {
|
[true, false].forEach(isPublic => describe(`(for ${isPublic ? 'public' : 'hidden'} builds)`, () => {
|
||||||
h.createDummyBuild(pr, sha9);
|
const authorizationHeader2 = isPublic ?
|
||||||
expect(h.readBuildFile(pr, sha9, 'index.html')).toContain('index.html');
|
authorizationHeader : '--header "Authorization: FAKE_VERIFIED_NOT_TRUSTED"';
|
||||||
|
const cmdPrefix = curl('', `${authorizationHeader2} ${xFileHeader}`);
|
||||||
h.writeBuildFile(pr, sha9, 'index.html', 'My content');
|
|
||||||
expect(h.readBuildFile(pr, sha9, 'index.html')).toBe('My content');
|
|
||||||
|
|
||||||
h.runCmd(`${curl} http://${host}/create-build/${pr}/${sha9}`).
|
|
||||||
then(h.verifyResponse(409, /^Request to overwrite existing directory/)).
|
|
||||||
then(() => expect(h.readBuildFile(pr, sha9, 'index.html')).toBe('My content')).
|
|
||||||
then(done);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
it('should delete the PR directory on error (for new PR)', done => {
|
it('should not overwrite existing builds', done => {
|
||||||
const prDir = path.join(h.buildsDir, pr);
|
h.createDummyBuild(pr, sha9, isPublic);
|
||||||
|
expect(h.readBuildFile(pr, sha9, 'index.html', isPublic)).toContain('index.html');
|
||||||
|
|
||||||
h.runCmd(`${curl} http://${host}/create-build/${pr}/${sha9}`).
|
h.writeBuildFile(pr, sha9, 'index.html', 'My content', isPublic);
|
||||||
then(h.verifyResponse(500)).
|
expect(h.readBuildFile(pr, sha9, 'index.html', isPublic)).toBe('My content');
|
||||||
then(() => expect(fs.existsSync(prDir)).toBe(false)).
|
|
||||||
then(done);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
h.runCmd(`${cmdPrefix} http://${host}/create-build/${pr}/${sha9}`).
|
||||||
it('should only delete the SHA directory on error (for existing PR)', done => {
|
then(h.verifyResponse(409, /^Request to overwrite existing directory/)).
|
||||||
const prDir = path.join(h.buildsDir, pr);
|
then(() => expect(h.readBuildFile(pr, sha9, 'index.html', isPublic)).toBe('My content')).
|
||||||
const shaDir = path.join(prDir, sha9);
|
then(done);
|
||||||
|
|
||||||
h.createDummyBuild(pr, sha0);
|
|
||||||
|
|
||||||
h.runCmd(`${curl} http://${host}/create-build/${pr}/${sha9}`).
|
|
||||||
then(h.verifyResponse(500)).
|
|
||||||
then(() => {
|
|
||||||
expect(fs.existsSync(shaDir)).toBe(false);
|
|
||||||
expect(fs.existsSync(prDir)).toBe(true);
|
|
||||||
}).
|
|
||||||
then(done);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
describe('on successful upload', () => {
|
|
||||||
const archivePath = path.join(h.buildsDir, 'snapshot.tar.gz');
|
|
||||||
let uploadPromise: Promise<CmdResult>;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
h.createDummyArchive(pr, sha9, archivePath);
|
|
||||||
uploadPromise = h.runCmd(`${curl} http://${host}/create-build/${pr}/${sha9}`);
|
|
||||||
});
|
|
||||||
afterEach(() => h.deletePrDir(pr));
|
|
||||||
|
|
||||||
|
|
||||||
it('should respond with 201', done => {
|
|
||||||
uploadPromise.then(h.verifyResponse(201)).then(done);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
it('should extract the contents of the uploaded file', done => {
|
it('should not overwrite existing builds (even if the SHA is different)', done => {
|
||||||
uploadPromise.
|
// 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, /^Request to overwrite existing directory/)).
|
||||||
|
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(() => {
|
then(() => {
|
||||||
expect(h.readBuildFile(pr, sha9, 'index.html')).toContain(`uploaded/${pr}`);
|
expect(h.buildExists(pr, sha9, isPublic)).toBe(false);
|
||||||
expect(h.readBuildFile(pr, sha9, 'foo/bar.js')).toContain(`uploaded/${pr}`);
|
expect(h.buildExists(pr, '', isPublic)).toBe(true);
|
||||||
}).
|
}).
|
||||||
then(done);
|
then(done);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
it(`should create files/directories owned by '${h.wwwUser}'`, done => {
|
describe('on successful upload', () => {
|
||||||
const shaDir = path.join(h.buildsDir, pr, sha9);
|
const archivePath = path.join(h.buildsDir, 'snapshot.tar.gz');
|
||||||
const idxPath = path.join(shaDir, 'index.html');
|
const statusCode = isPublic ? 201 : 202;
|
||||||
const barPath = path.join(shaDir, 'foo', 'bar.js');
|
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);
|
||||||
|
});
|
||||||
|
|
||||||
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 => {
|
describe('when the PR\'s visibility has changed', () => {
|
||||||
expect(fs.existsSync(archivePath)).toBe(true);
|
const archivePath = path.join(h.buildsDir, 'snapshot.tar.gz');
|
||||||
uploadPromise.
|
const statusCode = isPublic ? 201 : 202;
|
||||||
then(() => expect(fs.existsSync(archivePath)).toBe(false)).
|
|
||||||
then(done);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
const checkPrVisibility = (isPublic2: boolean) => {
|
||||||
it('should make the build directory non-writable', done => {
|
expect(h.buildExists(pr, '', isPublic2)).toBe(true);
|
||||||
const shaDir = path.join(h.buildsDir, pr, sha9);
|
expect(h.buildExists(pr, '', !isPublic2)).toBe(false);
|
||||||
const idxPath = path.join(shaDir, 'index.html');
|
expect(h.buildExists(pr, sha0, isPublic2)).toBe(true);
|
||||||
const barPath = path.join(shaDir, 'foo', 'bar.js');
|
expect(h.buildExists(pr, sha0, !isPublic2)).toBe(false);
|
||||||
|
|
||||||
// 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 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, /^Request to overwrite existing directory/)).
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
|
||||||
uploadPromise.
|
|
||||||
then(() => {
|
|
||||||
expect(isNotWritable(shaDir)).toBe(true);
|
|
||||||
expect(isNotWritable(idxPath)).toBe(true);
|
|
||||||
expect(isNotWritable(barPath)).toBe(true);
|
|
||||||
}).
|
|
||||||
then(done);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
}));
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
// Imports
|
// Imports
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
|
import * as path 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 {GithubPullRequests} from '../../lib/common/github-pull-requests';
|
import {GithubPullRequests} from '../../lib/common/github-pull-requests';
|
||||||
|
|
||||||
// Tests
|
// Tests
|
||||||
@ -114,7 +116,7 @@ describe('BuildCleaner', () => {
|
|||||||
|
|
||||||
it('should resolve with the value returned by \'removeUnnecessaryBuilds()\'', done => {
|
it('should resolve with the value returned by \'removeUnnecessaryBuilds()\'', done => {
|
||||||
promise.then(result => {
|
promise.then(result => {
|
||||||
expect(result).toBe('Test');
|
expect(result as any).toBe('Test');
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -170,6 +172,16 @@ describe('BuildCleaner', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should remove `HIDDEN_DIR_PREFIX` from the filenames', done => {
|
||||||
|
promise.then(result => {
|
||||||
|
expect(result).toEqual([12, 34, 56]);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
readdirCb(null, [`${HIDDEN_DIR_PREFIX}12`, '34', `${HIDDEN_DIR_PREFIX}56`]);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
it('should ignore files with non-numeric (or zero) names', done => {
|
it('should ignore files with non-numeric (or zero) names', done => {
|
||||||
promise.then(result => {
|
promise.then(result => {
|
||||||
expect(result).toEqual([12, 34, 56]);
|
expect(result).toEqual([12, 34, 56]);
|
||||||
@ -230,10 +242,22 @@ describe('BuildCleaner', () => {
|
|||||||
describe('removeDir()', () => {
|
describe('removeDir()', () => {
|
||||||
let shellChmodSpy: jasmine.Spy;
|
let shellChmodSpy: jasmine.Spy;
|
||||||
let shellRmSpy: jasmine.Spy;
|
let shellRmSpy: jasmine.Spy;
|
||||||
|
let shellTestSpy: jasmine.Spy;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
shellChmodSpy = spyOn(shell, 'chmod');
|
shellChmodSpy = spyOn(shell, 'chmod');
|
||||||
shellRmSpy = spyOn(shell, 'rm');
|
shellRmSpy = spyOn(shell, 'rm');
|
||||||
|
shellTestSpy = spyOn(shell, 'test').and.returnValue(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should test if the directory exists (and return if is does not)', () => {
|
||||||
|
shellTestSpy.and.returnValue(false);
|
||||||
|
(cleaner as any).removeDir('/foo/bar');
|
||||||
|
|
||||||
|
expect(shellTestSpy).toHaveBeenCalledWith('-d', '/foo/bar');
|
||||||
|
expect(shellChmodSpy).not.toHaveBeenCalled();
|
||||||
|
expect(shellRmSpy).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
@ -287,17 +311,28 @@ describe('BuildCleaner', () => {
|
|||||||
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 as any).removeUnnecessaryBuilds([1, 2, 3], []);
|
||||||
|
|
||||||
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith('/foo/bar/1');
|
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(path.normalize('/foo/bar/1'));
|
||||||
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith('/foo/bar/2');
|
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(path.normalize('/foo/bar/2'));
|
||||||
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith('/foo/bar/3');
|
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(path.normalize('/foo/bar/3'));
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should try removing hidden directories as well', () => {
|
||||||
|
(cleaner as any).removeUnnecessaryBuilds([1, 2, 3], []);
|
||||||
|
|
||||||
|
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(path.normalize(`/foo/bar/${HIDDEN_DIR_PREFIX}1`));
|
||||||
|
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(path.normalize(`/foo/bar/${HIDDEN_DIR_PREFIX}2`));
|
||||||
|
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(path.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 as any).removeUnnecessaryBuilds([1, 2, 3, 4], [2, 4]);
|
||||||
expect(cleanerRemoveDirSpy).toHaveBeenCalledTimes(2);
|
expect(cleanerRemoveDirSpy).toHaveBeenCalledTimes(4);
|
||||||
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith('/foo/bar/1');
|
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(path.normalize('/foo/bar/1'));
|
||||||
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith('/foo/bar/3');
|
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(path.normalize('/foo/bar/3'));
|
||||||
|
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(path.normalize(`/foo/bar/${HIDDEN_DIR_PREFIX}1`));
|
||||||
|
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(path.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 as any).removeUnnecessaryBuilds([1, 2, 3, 4], [1, 2, 3, 4]);
|
||||||
@ -305,11 +340,15 @@ describe('BuildCleaner', () => {
|
|||||||
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(4);
|
expect(cleanerRemoveDirSpy).toHaveBeenCalledTimes(8);
|
||||||
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith('/foo/bar/1');
|
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(path.normalize('/foo/bar/1'));
|
||||||
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith('/foo/bar/2');
|
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(path.normalize('/foo/bar/2'));
|
||||||
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith('/foo/bar/3');
|
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(path.normalize('/foo/bar/3'));
|
||||||
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith('/foo/bar/4');
|
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(path.normalize('/foo/bar/4'));
|
||||||
|
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(path.normalize(`/foo/bar/${HIDDEN_DIR_PREFIX}1`));
|
||||||
|
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(path.normalize(`/foo/bar/${HIDDEN_DIR_PREFIX}2`));
|
||||||
|
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(path.normalize(`/foo/bar/${HIDDEN_DIR_PREFIX}3`));
|
||||||
|
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(path.normalize(`/foo/bar/${HIDDEN_DIR_PREFIX}4`));
|
||||||
cleanerRemoveDirSpy.calls.reset();
|
cleanerRemoveDirSpy.calls.reset();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -292,7 +292,7 @@ describe('GithubApi', () => {
|
|||||||
|
|
||||||
|
|
||||||
describe('onResponse', () => {
|
describe('onResponse', () => {
|
||||||
let promise: Promise<void>;
|
let promise: Promise<Object>;
|
||||||
let respond: (statusCode: number) => IncomingMessage;
|
let respond: (statusCode: number) => IncomingMessage;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
@ -66,7 +66,7 @@ describe('GithubPullRequests', () => {
|
|||||||
|
|
||||||
it('should resolve with the returned response', done => {
|
it('should resolve with the returned response', done => {
|
||||||
prs.addComment(42, 'body').then(data => {
|
prs.addComment(42, 'body').then(data => {
|
||||||
expect(data).toEqual('Test');
|
expect(data as any).toBe('Test');
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -76,6 +76,30 @@ describe('GithubPullRequests', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
describe('fetch()', () => {
|
||||||
|
let prs: GithubPullRequests;
|
||||||
|
let prsGetSpy: jasmine.Spy;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
prs = new GithubPullRequests('12345', 'foo/bar');
|
||||||
|
prsGetSpy = spyOn(prs as any, 'get');
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should call \'get()\' with the correct pathname', () => {
|
||||||
|
prs.fetch(42);
|
||||||
|
expect(prsGetSpy).toHaveBeenCalledWith('/repos/foo/bar/issues/42');
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should forward the value returned by \'get()\'', () => {
|
||||||
|
prsGetSpy.and.returnValue('Test');
|
||||||
|
expect(prs.fetch(42) as any).toBe('Test');
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
describe('fetchAll()', () => {
|
describe('fetchAll()', () => {
|
||||||
let prs: GithubPullRequests;
|
let prs: GithubPullRequests;
|
||||||
let prsGetPaginatedSpy: jasmine.Spy;
|
let prsGetPaginatedSpy: jasmine.Spy;
|
||||||
@ -109,7 +133,7 @@ describe('GithubPullRequests', () => {
|
|||||||
|
|
||||||
it('should forward the value returned by \'getPaginated()\'', () => {
|
it('should forward the value returned by \'getPaginated()\'', () => {
|
||||||
prsGetPaginatedSpy.and.returnValue('Test');
|
prsGetPaginatedSpy.and.returnValue('Test');
|
||||||
expect(prs.fetchAll()).toBe('Test');
|
expect(prs.fetchAll() as any).toBe('Test');
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
@ -38,7 +38,7 @@ describe('GithubTeams', () => {
|
|||||||
|
|
||||||
it('should forward the value returned by \'getPaginated()\'', () => {
|
it('should forward the value returned by \'getPaginated()\'', () => {
|
||||||
teamsGetPaginatedSpy.and.returnValue('Test');
|
teamsGetPaginatedSpy.and.returnValue('Test');
|
||||||
expect(teams.fetchAll()).toBe('Test');
|
expect(teams.fetchAll() as any).toBe('Test');
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
@ -50,12 +50,16 @@ describe('GithubTeams', () => {
|
|||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
teams = new GithubTeams('12345', 'foo');
|
teams = new GithubTeams('12345', 'foo');
|
||||||
teamsGetSpy = spyOn(teams, 'get');
|
teamsGetSpy = spyOn(teams, 'get').and.returnValue(Promise.resolve(null));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
it('should return a promise', () => {
|
it('should return a promise', done => {
|
||||||
expect(teams.isMemberById('user', [1])).toEqual(jasmine.any(Promise));
|
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));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
@ -69,7 +73,6 @@ describe('GithubTeams', () => {
|
|||||||
|
|
||||||
|
|
||||||
it('should call \'get()\' with the correct pathname', done => {
|
it('should call \'get()\' with the correct pathname', done => {
|
||||||
teamsGetSpy.and.returnValue(Promise.resolve(null));
|
|
||||||
teams.isMemberById('user', [1]).then(() => {
|
teams.isMemberById('user', [1]).then(() => {
|
||||||
expect(teamsGetSpy).toHaveBeenCalledWith('/teams/1/memberships/user');
|
expect(teamsGetSpy).toHaveBeenCalledWith('/teams/1/memberships/user');
|
||||||
done();
|
done();
|
||||||
|
@ -2,9 +2,11 @@
|
|||||||
import * as cp from 'child_process';
|
import * as cp from 'child_process';
|
||||||
import {EventEmitter} from 'events';
|
import {EventEmitter} from 'events';
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
import * as shell from 'shelljs';
|
import * as shell from 'shelljs';
|
||||||
|
import {SHORT_SHA_LEN} from '../../lib/common/constants';
|
||||||
import {BuildCreator} from '../../lib/upload-server/build-creator';
|
import {BuildCreator} from '../../lib/upload-server/build-creator';
|
||||||
import {CreatedBuildEvent} from '../../lib/upload-server/build-events';
|
import {ChangedPrVisibilityEvent, CreatedBuildEvent} from '../../lib/upload-server/build-events';
|
||||||
import {UploadError} from '../../lib/upload-server/upload-error';
|
import {UploadError} from '../../lib/upload-server/upload-error';
|
||||||
import {expectToBeUploadError} from './helpers';
|
import {expectToBeUploadError} from './helpers';
|
||||||
|
|
||||||
@ -12,10 +14,13 @@ import {expectToBeUploadError} from './helpers';
|
|||||||
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 archive = 'snapshot.tar.gz';
|
const archive = 'snapshot.tar.gz';
|
||||||
const buildsDir = 'builds/dir';
|
const buildsDir = 'builds/dir';
|
||||||
const prDir = `${buildsDir}/${pr}`;
|
const hiddenPrDir = path.join(buildsDir, `hidden--${pr}`);
|
||||||
const shaDir = `${prDir}/${sha}`;
|
const publicPrDir = path.join(buildsDir, pr);
|
||||||
|
const hiddenShaDir = path.join(hiddenPrDir, shortSha);
|
||||||
|
const publicShaDir = path.join(publicPrDir, shortSha);
|
||||||
let bc: BuildCreator;
|
let bc: BuildCreator;
|
||||||
|
|
||||||
beforeEach(() => bc = new BuildCreator(buildsDir));
|
beforeEach(() => bc = new BuildCreator(buildsDir));
|
||||||
@ -38,7 +43,160 @@ describe('BuildCreator', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
describe('changePrVisibility()', () => {
|
||||||
|
let bcEmitSpy: jasmine.Spy;
|
||||||
|
let bcExistsSpy: jasmine.Spy;
|
||||||
|
let bcListShasByDate: jasmine.Spy;
|
||||||
|
let shellMvSpy: jasmine.Spy;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
bcEmitSpy = spyOn(bc, 'emit');
|
||||||
|
bcExistsSpy = spyOn(bc as any, 'exists');
|
||||||
|
bcListShasByDate = spyOn(bc as any, 'listShasByDate');
|
||||||
|
shellMvSpy = spyOn(shell, 'mv');
|
||||||
|
|
||||||
|
bcExistsSpy.and.returnValues(Promise.resolve(true), Promise.resolve(false));
|
||||||
|
bcListShasByDate.and.returnValue([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should return a promise', done => {
|
||||||
|
const promise = bc.changePrVisibility(pr, true);
|
||||||
|
promise.then(done); // Do not complete the test (and release the spies) synchronously
|
||||||
|
// to avoid running the actual `extractArchive()`.
|
||||||
|
|
||||||
|
expect(promise).toEqual(jasmine.any(Promise));
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
[true, false].forEach(makePublic => {
|
||||||
|
const oldPrDir = makePublic ? hiddenPrDir : publicPrDir;
|
||||||
|
const newPrDir = makePublic ? publicPrDir : hiddenPrDir;
|
||||||
|
|
||||||
|
|
||||||
|
it('should rename the directory', done => {
|
||||||
|
bc.changePrVisibility(pr, makePublic).
|
||||||
|
then(() => expect(shellMvSpy).toHaveBeenCalledWith(oldPrDir, newPrDir)).
|
||||||
|
then(done);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should emit a ChangedPrVisibilityEvent on success', done => {
|
||||||
|
let emitted = false;
|
||||||
|
|
||||||
|
bcEmitSpy.and.callFake((type: string, evt: ChangedPrVisibilityEvent) => {
|
||||||
|
expect(type).toBe(ChangedPrVisibilityEvent.type);
|
||||||
|
expect(evt).toEqual(jasmine.any(ChangedPrVisibilityEvent));
|
||||||
|
expect(evt.pr).toBe(+pr);
|
||||||
|
expect(evt.shas).toEqual(jasmine.any(Array));
|
||||||
|
expect(evt.isPublic).toBe(makePublic);
|
||||||
|
|
||||||
|
emitted = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
bc.changePrVisibility(pr, makePublic).
|
||||||
|
then(() => expect(emitted).toBe(true)).
|
||||||
|
then(done);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should include all shas in the emitted event', done => {
|
||||||
|
const shas = ['foo', 'bar', 'baz'];
|
||||||
|
let emitted = false;
|
||||||
|
|
||||||
|
bcListShasByDate.and.returnValue(Promise.resolve(shas));
|
||||||
|
bcEmitSpy.and.callFake((type: string, evt: ChangedPrVisibilityEvent) => {
|
||||||
|
expect(bcListShasByDate).toHaveBeenCalledWith(newPrDir);
|
||||||
|
|
||||||
|
expect(type).toBe(ChangedPrVisibilityEvent.type);
|
||||||
|
expect(evt).toEqual(jasmine.any(ChangedPrVisibilityEvent));
|
||||||
|
expect(evt.pr).toBe(+pr);
|
||||||
|
expect(evt.shas).toBe(shas);
|
||||||
|
expect(evt.isPublic).toBe(makePublic);
|
||||||
|
|
||||||
|
emitted = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
bc.changePrVisibility(pr, makePublic).
|
||||||
|
then(() => expect(emitted).toBe(true)).
|
||||||
|
then(done);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
describe('on error', () => {
|
||||||
|
|
||||||
|
it('should abort and skip further operations if the old directory does not exist', done => {
|
||||||
|
bcExistsSpy.and.callFake((dir: string) => dir !== oldPrDir);
|
||||||
|
bc.changePrVisibility(pr, makePublic).catch(err => {
|
||||||
|
expectToBeUploadError(err, 404, `Request to move non-existing directory '${oldPrDir}' to '${newPrDir}'.`);
|
||||||
|
expect(shellMvSpy).not.toHaveBeenCalled();
|
||||||
|
expect(bcListShasByDate).not.toHaveBeenCalled();
|
||||||
|
expect(bcEmitSpy).not.toHaveBeenCalled();
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should abort and skip further operations if the new directory does already exist', done => {
|
||||||
|
bcExistsSpy.and.returnValue(true);
|
||||||
|
bc.changePrVisibility(pr, makePublic).catch(err => {
|
||||||
|
expectToBeUploadError(err, 409, `Request to move '${oldPrDir}' to existing directory '${newPrDir}'.`);
|
||||||
|
expect(shellMvSpy).not.toHaveBeenCalled();
|
||||||
|
expect(bcListShasByDate).not.toHaveBeenCalled();
|
||||||
|
expect(bcEmitSpy).not.toHaveBeenCalled();
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should abort and skip further operations if it fails to rename the directory', done => {
|
||||||
|
shellMvSpy.and.throwError('');
|
||||||
|
bc.changePrVisibility(pr, makePublic).catch(() => {
|
||||||
|
expect(shellMvSpy).toHaveBeenCalled();
|
||||||
|
expect(bcListShasByDate).not.toHaveBeenCalled();
|
||||||
|
expect(bcEmitSpy).not.toHaveBeenCalled();
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should abort and skip further operations if it fails to list the SHAs', done => {
|
||||||
|
bcListShasByDate.and.throwError('');
|
||||||
|
bc.changePrVisibility(pr, makePublic).catch(() => {
|
||||||
|
expect(shellMvSpy).toHaveBeenCalled();
|
||||||
|
expect(bcListShasByDate).toHaveBeenCalled();
|
||||||
|
expect(bcEmitSpy).not.toHaveBeenCalled();
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should reject with an UploadError', done => {
|
||||||
|
shellMvSpy.and.callFake(() => { throw 'Test'; });
|
||||||
|
bc.changePrVisibility(pr, makePublic).catch(err => {
|
||||||
|
expectToBeUploadError(err, 500, `Error while making PR ${pr} ${makePublic ? 'public' : 'hidden'}.\nTest`);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should pass UploadError instances unmodified', done => {
|
||||||
|
shellMvSpy.and.callFake(() => { throw new UploadError(543, 'Test'); });
|
||||||
|
bc.changePrVisibility(pr, makePublic).catch(err => {
|
||||||
|
expectToBeUploadError(err, 543, 'Test');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
describe('create()', () => {
|
describe('create()', () => {
|
||||||
|
let bcChangePrVisibilitySpy: jasmine.Spy;
|
||||||
let bcEmitSpy: jasmine.Spy;
|
let bcEmitSpy: jasmine.Spy;
|
||||||
let bcExistsSpy: jasmine.Spy;
|
let bcExistsSpy: jasmine.Spy;
|
||||||
let bcExtractArchiveSpy: jasmine.Spy;
|
let bcExtractArchiveSpy: jasmine.Spy;
|
||||||
@ -46,6 +204,7 @@ describe('BuildCreator', () => {
|
|||||||
let shellRmSpy: jasmine.Spy;
|
let shellRmSpy: jasmine.Spy;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
bcChangePrVisibilitySpy = spyOn(bc, 'changePrVisibility');
|
||||||
bcEmitSpy = spyOn(bc, 'emit');
|
bcEmitSpy = spyOn(bc, 'emit');
|
||||||
bcExistsSpy = spyOn(bc as any, 'exists');
|
bcExistsSpy = spyOn(bc as any, 'exists');
|
||||||
bcExtractArchiveSpy = spyOn(bc as any, 'extractArchive');
|
bcExtractArchiveSpy = spyOn(bc as any, 'extractArchive');
|
||||||
@ -54,115 +213,192 @@ describe('BuildCreator', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
it('should return a promise', done => {
|
[true, false].forEach(isPublic => {
|
||||||
const promise = bc.create(pr, sha, archive);
|
const otherVisPrDir = isPublic ? hiddenPrDir : publicPrDir;
|
||||||
promise.then(done); // Do not complete the test (and release the spies) synchronously
|
const prDir = isPublic ? publicPrDir : hiddenPrDir;
|
||||||
// to avoid running the actual `extractArchive()`.
|
const shaDir = isPublic ? publicShaDir : hiddenShaDir;
|
||||||
|
|
||||||
expect(promise).toEqual(jasmine.any(Promise));
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
it('should throw if the build does already exist', done => {
|
it('should return a promise', done => {
|
||||||
bcExistsSpy.and.returnValue(true);
|
const promise = bc.create(pr, sha, archive, isPublic);
|
||||||
bc.create(pr, sha, archive).catch(err => {
|
promise.then(done); // Do not complete the test (and release the spies) synchronously
|
||||||
expectToBeUploadError(err, 409, `Request to overwrite existing directory: ${shaDir}`);
|
// to avoid running the actual `extractArchive()`.
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
|
expect(promise).toEqual(jasmine.any(Promise));
|
||||||
it('should create the build directory (and any parent directories)', done => {
|
|
||||||
bc.create(pr, sha, archive).
|
|
||||||
then(() => expect(shellMkdirSpy).toHaveBeenCalledWith('-p', shaDir)).
|
|
||||||
then(done);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
it('should extract the archive contents into the build directory', done => {
|
|
||||||
bc.create(pr, sha, archive).
|
|
||||||
then(() => expect(bcExtractArchiveSpy).toHaveBeenCalledWith(archive, shaDir)).
|
|
||||||
then(done);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
it('should emit a CreatedBuildEvent on success', done => {
|
|
||||||
let emitted = false;
|
|
||||||
|
|
||||||
bcEmitSpy.and.callFake((type: string, evt: CreatedBuildEvent) => {
|
|
||||||
expect(type).toBe(CreatedBuildEvent.type);
|
|
||||||
expect(evt).toEqual(jasmine.any(CreatedBuildEvent));
|
|
||||||
expect(evt.pr).toBe(+pr);
|
|
||||||
expect(evt.sha).toBe(sha);
|
|
||||||
|
|
||||||
emitted = true;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
bc.create(pr, sha, archive).
|
|
||||||
then(() => expect(emitted).toBe(true)).
|
it('should not update the PR\'s visibility first if not necessary', done => {
|
||||||
then(done);
|
bc.create(pr, sha, archive, isPublic).
|
||||||
});
|
then(() => expect(bcChangePrVisibilitySpy).not.toHaveBeenCalled()).
|
||||||
|
then(done);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
describe('on error', () => {
|
it('should update the PR\'s visibility first if necessary', done => {
|
||||||
|
bcChangePrVisibilitySpy.and.callFake(() => expect(shellMkdirSpy).not.toHaveBeenCalled());
|
||||||
|
bcExistsSpy.and.callFake((dir: string) => dir === otherVisPrDir);
|
||||||
|
|
||||||
it('should abort and skip further operations if it fails to create the directories', done => {
|
bc.create(pr, sha, archive, isPublic).
|
||||||
shellMkdirSpy.and.throwError('');
|
then(() => {
|
||||||
bc.create(pr, sha, archive).catch(() => {
|
expect(bcChangePrVisibilitySpy).toHaveBeenCalledWith(pr, isPublic);
|
||||||
expect(shellMkdirSpy).toHaveBeenCalled();
|
expect(shellMkdirSpy).toHaveBeenCalled();
|
||||||
expect(bcExtractArchiveSpy).not.toHaveBeenCalled();
|
}).
|
||||||
expect(bcEmitSpy).not.toHaveBeenCalled();
|
then(done);
|
||||||
done();
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should create the build directory (and any parent directories)', done => {
|
||||||
|
bc.create(pr, sha, archive, isPublic).
|
||||||
|
then(() => expect(shellMkdirSpy).toHaveBeenCalledWith('-p', shaDir)).
|
||||||
|
then(done);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should extract the archive contents into the build directory', done => {
|
||||||
|
bc.create(pr, sha, archive, isPublic).
|
||||||
|
then(() => expect(bcExtractArchiveSpy).toHaveBeenCalledWith(archive, shaDir)).
|
||||||
|
then(done);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should emit a CreatedBuildEvent on success', done => {
|
||||||
|
let emitted = false;
|
||||||
|
|
||||||
|
bcEmitSpy.and.callFake((type: string, evt: CreatedBuildEvent) => {
|
||||||
|
expect(type).toBe(CreatedBuildEvent.type);
|
||||||
|
expect(evt).toEqual(jasmine.any(CreatedBuildEvent));
|
||||||
|
expect(evt.pr).toBe(+pr);
|
||||||
|
expect(evt.sha).toBe(shortSha);
|
||||||
|
expect(evt.isPublic).toBe(isPublic);
|
||||||
|
|
||||||
|
emitted = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
bc.create(pr, sha, archive, isPublic).
|
||||||
|
then(() => expect(emitted).toBe(true)).
|
||||||
|
then(done);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
it('should abort and skip further operations if it fails to extract the archive', done => {
|
describe('on error', () => {
|
||||||
bcExtractArchiveSpy.and.throwError('');
|
let existsValues: {[dir: string]: boolean};
|
||||||
bc.create(pr, sha, archive).catch(() => {
|
|
||||||
expect(shellMkdirSpy).toHaveBeenCalled();
|
beforeEach(() => {
|
||||||
expect(bcExtractArchiveSpy).toHaveBeenCalled();
|
existsValues = {
|
||||||
expect(bcEmitSpy).not.toHaveBeenCalled();
|
[otherVisPrDir]: false,
|
||||||
done();
|
[prDir]: false,
|
||||||
|
[shaDir]: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
bcExistsSpy.and.callFake((dir: string) => existsValues[dir]);
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
it('should delete the PR directory (for new PR)', done => {
|
it('should abort and skip further operations if changing the PR\'s visibility fails', done => {
|
||||||
bcExtractArchiveSpy.and.throwError('');
|
const mockError = new UploadError(543, 'Test');
|
||||||
bc.create(pr, sha, archive).catch(() => {
|
|
||||||
expect(shellRmSpy).toHaveBeenCalledWith('-rf', prDir);
|
existsValues[otherVisPrDir] = true;
|
||||||
done();
|
bcChangePrVisibilitySpy.and.returnValue(Promise.reject(mockError));
|
||||||
|
|
||||||
|
bc.create(pr, sha, archive, isPublic).catch(err => {
|
||||||
|
expect(err).toBe(mockError);
|
||||||
|
|
||||||
|
expect(bcExistsSpy).toHaveBeenCalledTimes(1);
|
||||||
|
expect(shellMkdirSpy).not.toHaveBeenCalled();
|
||||||
|
expect(bcExtractArchiveSpy).not.toHaveBeenCalled();
|
||||||
|
expect(bcEmitSpy).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
done();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
it('should delete the SHA directory (for existing PR)', done => {
|
it('should abort and skip further operations if the build does already exist', done => {
|
||||||
bcExistsSpy.and.callFake((path: string) => path !== shaDir);
|
existsValues[shaDir] = true;
|
||||||
bcExtractArchiveSpy.and.throwError('');
|
bc.create(pr, sha, archive, isPublic).catch(err => {
|
||||||
|
expectToBeUploadError(err, 409, `Request to overwrite existing directory: ${shaDir}`);
|
||||||
bc.create(pr, sha, archive).catch(() => {
|
expect(shellMkdirSpy).not.toHaveBeenCalled();
|
||||||
expect(shellRmSpy).toHaveBeenCalledWith('-rf', shaDir);
|
expect(bcExtractArchiveSpy).not.toHaveBeenCalled();
|
||||||
done();
|
expect(bcEmitSpy).not.toHaveBeenCalled();
|
||||||
|
done();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
it('should reject with an UploadError', done => {
|
it('should detect existing build directory after visibility change', done => {
|
||||||
shellMkdirSpy.and.callFake(() => {throw 'Test'; });
|
existsValues[otherVisPrDir] = true;
|
||||||
bc.create(pr, sha, archive).catch(err => {
|
bcChangePrVisibilitySpy.and.callFake(() => existsValues[prDir] = existsValues[shaDir] = true);
|
||||||
expectToBeUploadError(err, 500, `Error while uploading to directory: ${shaDir}\nTest`);
|
|
||||||
done();
|
bc.create(pr, sha, archive, isPublic).catch(err => {
|
||||||
|
expectToBeUploadError(err, 409, `Request to overwrite existing directory: ${shaDir}`);
|
||||||
|
expect(shellMkdirSpy).not.toHaveBeenCalled();
|
||||||
|
expect(bcExtractArchiveSpy).not.toHaveBeenCalled();
|
||||||
|
expect(bcEmitSpy).not.toHaveBeenCalled();
|
||||||
|
done();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
it('should pass UploadError instances unmodified', done => {
|
it('should abort and skip further operations if it fails to create the directories', done => {
|
||||||
shellMkdirSpy.and.callFake(() => { throw new UploadError(543, 'Test'); });
|
shellMkdirSpy.and.throwError('');
|
||||||
bc.create(pr, sha, archive).catch(err => {
|
bc.create(pr, sha, archive, isPublic).catch(() => {
|
||||||
expectToBeUploadError(err, 543, 'Test');
|
expect(shellMkdirSpy).toHaveBeenCalled();
|
||||||
done();
|
expect(bcExtractArchiveSpy).not.toHaveBeenCalled();
|
||||||
|
expect(bcEmitSpy).not.toHaveBeenCalled();
|
||||||
|
done();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should abort and skip further operations if it fails to extract the archive', done => {
|
||||||
|
bcExtractArchiveSpy.and.throwError('');
|
||||||
|
bc.create(pr, sha, archive, isPublic).catch(() => {
|
||||||
|
expect(shellMkdirSpy).toHaveBeenCalled();
|
||||||
|
expect(bcExtractArchiveSpy).toHaveBeenCalled();
|
||||||
|
expect(bcEmitSpy).not.toHaveBeenCalled();
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should delete the PR directory (for new PR)', done => {
|
||||||
|
bcExtractArchiveSpy.and.throwError('');
|
||||||
|
bc.create(pr, sha, archive, isPublic).catch(() => {
|
||||||
|
expect(shellRmSpy).toHaveBeenCalledWith('-rf', prDir);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should delete the SHA directory (for existing PR)', done => {
|
||||||
|
existsValues[prDir] = true;
|
||||||
|
bcExtractArchiveSpy.and.throwError('');
|
||||||
|
|
||||||
|
bc.create(pr, sha, archive, isPublic).catch(() => {
|
||||||
|
expect(shellRmSpy).toHaveBeenCalledWith('-rf', shaDir);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should reject with an UploadError', done => {
|
||||||
|
shellMkdirSpy.and.callFake(() => { throw 'Test'; });
|
||||||
|
bc.create(pr, sha, archive, isPublic).catch(err => {
|
||||||
|
expectToBeUploadError(err, 500, `Error while uploading to directory: ${shaDir}\nTest`);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should pass UploadError instances unmodified', done => {
|
||||||
|
shellMkdirSpy.and.callFake(() => { throw new UploadError(543, 'Test'); });
|
||||||
|
bc.create(pr, sha, archive, isPublic).catch(err => {
|
||||||
|
expectToBeUploadError(err, 543, 'Test');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
@ -317,4 +553,101 @@ describe('BuildCreator', () => {
|
|||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
describe('listShasByDate()', () => {
|
||||||
|
let shellLsSpy: jasmine.Spy;
|
||||||
|
const lsResult = (name: string, mtimeMs: number, isDirectory = true) => ({
|
||||||
|
isDirectory: () => isDirectory,
|
||||||
|
mtime: new Date(mtimeMs),
|
||||||
|
name,
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
shellLsSpy = spyOn(shell, 'ls').and.returnValue([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should return a promise', done => {
|
||||||
|
const promise = (bc as any).listShasByDate('input/dir');
|
||||||
|
promise.then(done); // Do not complete the test (and release the spies) synchronously
|
||||||
|
// to avoid running the actual `ls()`.
|
||||||
|
|
||||||
|
expect(promise).toEqual(jasmine.any(Promise));
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should `ls()` files with their metadata', done => {
|
||||||
|
(bc as any).listShasByDate('input/dir').
|
||||||
|
then(() => expect(shellLsSpy).toHaveBeenCalledWith('-l', 'input/dir')).
|
||||||
|
then(done);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should reject if listing files fails', done => {
|
||||||
|
shellLsSpy.and.returnValue(Promise.reject('Test'));
|
||||||
|
(bc as any).listShasByDate('input/dir').catch((err: string) => {
|
||||||
|
expect(err).toBe('Test');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should return the filenames', done => {
|
||||||
|
shellLsSpy.and.returnValue(Promise.resolve([
|
||||||
|
lsResult('foo', 100),
|
||||||
|
lsResult('bar', 200),
|
||||||
|
lsResult('baz', 300),
|
||||||
|
]));
|
||||||
|
|
||||||
|
(bc as any).listShasByDate('input/dir').
|
||||||
|
then((shas: string[]) => expect(shas).toEqual(['foo', 'bar', 'baz'])).
|
||||||
|
then(done);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should sort by date', done => {
|
||||||
|
shellLsSpy.and.returnValue(Promise.resolve([
|
||||||
|
lsResult('foo', 300),
|
||||||
|
lsResult('bar', 100),
|
||||||
|
lsResult('baz', 200),
|
||||||
|
]));
|
||||||
|
|
||||||
|
(bc as any).listShasByDate('input/dir').
|
||||||
|
then((shas: string[]) => expect(shas).toEqual(['bar', 'baz', 'foo'])).
|
||||||
|
then(done);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should not break with ShellJS\' custom `sort()` method', done => {
|
||||||
|
const mockArray = [
|
||||||
|
lsResult('foo', 300),
|
||||||
|
lsResult('bar', 100),
|
||||||
|
lsResult('baz', 200),
|
||||||
|
];
|
||||||
|
mockArray.sort = jasmine.createSpy('sort');
|
||||||
|
|
||||||
|
shellLsSpy.and.returnValue(Promise.resolve(mockArray));
|
||||||
|
(bc as any).listShasByDate('input/dir').
|
||||||
|
then((shas: string[]) => {
|
||||||
|
expect(shas).toEqual(['bar', 'baz', 'foo']);
|
||||||
|
expect(mockArray.sort).not.toHaveBeenCalled();
|
||||||
|
}).
|
||||||
|
then(done);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should only include directories', done => {
|
||||||
|
shellLsSpy.and.returnValue(Promise.resolve([
|
||||||
|
lsResult('foo', 100),
|
||||||
|
lsResult('bar', 200, false),
|
||||||
|
lsResult('baz', 300),
|
||||||
|
]));
|
||||||
|
|
||||||
|
(bc as any).listShasByDate('input/dir').
|
||||||
|
then((shas: string[]) => expect(shas).toEqual(['foo', 'baz'])).
|
||||||
|
then(done);
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
@ -1,15 +1,15 @@
|
|||||||
// Imports
|
// Imports
|
||||||
import {BuildEvent, CreatedBuildEvent} from '../../lib/upload-server/build-events';
|
import {ChangedPrVisibilityEvent, CreatedBuildEvent} from '../../lib/upload-server/build-events';
|
||||||
|
|
||||||
// Tests
|
// Tests
|
||||||
describe('BuildEvent', () => {
|
describe('ChangedPrVisibilityEvent', () => {
|
||||||
let evt: BuildEvent;
|
let evt: ChangedPrVisibilityEvent;
|
||||||
|
|
||||||
beforeEach(() => evt = new BuildEvent('foo', 42, 'bar'));
|
beforeEach(() => evt = new ChangedPrVisibilityEvent(42, ['foo', 'bar'], true));
|
||||||
|
|
||||||
|
|
||||||
it('should have a \'type\' property', () => {
|
it('should have a static \'type\' property', () => {
|
||||||
expect(evt.type).toBe('foo');
|
expect(ChangedPrVisibilityEvent.type).toBe('pr.changedVisibility');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
@ -18,8 +18,13 @@ describe('BuildEvent', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
it('should have a \'sha\' property', () => {
|
it('should have a \'shas\' property', () => {
|
||||||
expect(evt.sha).toBe('bar');
|
expect(evt.shas).toEqual(['foo', 'bar']);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should have an \'isPublic\' property', () => {
|
||||||
|
expect(evt.isPublic).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
@ -28,7 +33,7 @@ describe('BuildEvent', () => {
|
|||||||
describe('CreatedBuildEvent', () => {
|
describe('CreatedBuildEvent', () => {
|
||||||
let evt: CreatedBuildEvent;
|
let evt: CreatedBuildEvent;
|
||||||
|
|
||||||
beforeEach(() => evt = new CreatedBuildEvent(42, 'bar'));
|
beforeEach(() => evt = new CreatedBuildEvent(42, 'bar', true));
|
||||||
|
|
||||||
|
|
||||||
it('should have a static \'type\' property', () => {
|
it('should have a static \'type\' property', () => {
|
||||||
@ -36,19 +41,6 @@ describe('CreatedBuildEvent', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
it('should extend BuildEvent', () => {
|
|
||||||
expect(evt).toEqual(jasmine.any(CreatedBuildEvent));
|
|
||||||
expect(evt).toEqual(jasmine.any(BuildEvent));
|
|
||||||
|
|
||||||
expect(Object.getPrototypeOf(evt)).toBe(CreatedBuildEvent.prototype);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
it('should automatically set the \'type\'', () => {
|
|
||||||
expect(evt.type).toBe(CreatedBuildEvent.type);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
it('should have a \'pr\' property', () => {
|
it('should have a \'pr\' property', () => {
|
||||||
expect(evt.pr).toBe(42);
|
expect(evt.pr).toBe(42);
|
||||||
});
|
});
|
||||||
@ -58,4 +50,9 @@ describe('CreatedBuildEvent', () => {
|
|||||||
expect(evt.sha).toBe('bar');
|
expect(evt.sha).toBe('bar');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should have an \'isPublic\' property', () => {
|
||||||
|
expect(evt.isPublic).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
// Imports
|
// Imports
|
||||||
import * as jwt from 'jsonwebtoken';
|
import * as jwt from 'jsonwebtoken';
|
||||||
import {GithubPullRequests} from '../../lib/common/github-pull-requests';
|
import {GithubPullRequests, PullRequest} from '../../lib/common/github-pull-requests';
|
||||||
import {GithubTeams} from '../../lib/common/github-teams';
|
import {GithubTeams} from '../../lib/common/github-teams';
|
||||||
import {BuildVerifier} from '../../lib/upload-server/build-verifier';
|
import {BUILD_VERIFICATION_STATUS, BuildVerifier} from '../../lib/upload-server/build-verifier';
|
||||||
import {expectToBeUploadError} from './helpers';
|
import {expectToBeUploadError} from './helpers';
|
||||||
|
|
||||||
// Tests
|
// Tests
|
||||||
@ -13,14 +13,15 @@ describe('BuildVerifier', () => {
|
|||||||
organization: 'organization',
|
organization: 'organization',
|
||||||
repoSlug: 'repo/slug',
|
repoSlug: 'repo/slug',
|
||||||
secret: 'secret',
|
secret: 'secret',
|
||||||
|
trustedPrLabel: 'trusted: pr-label',
|
||||||
};
|
};
|
||||||
let bv: BuildVerifier;
|
let bv: BuildVerifier;
|
||||||
|
|
||||||
// Helpers
|
// Helpers
|
||||||
const createBuildVerifier = (partialConfig: Partial<typeof defaultConfig> = {}) => {
|
const createBuildVerifier = (partialConfig: Partial<typeof defaultConfig> = {}) => {
|
||||||
const cfg = {...defaultConfig, ...partialConfig};
|
const cfg = {...defaultConfig, ...partialConfig} as typeof defaultConfig;
|
||||||
return new BuildVerifier(cfg.secret, cfg.githubToken, cfg.repoSlug, cfg.organization,
|
return new BuildVerifier(cfg.secret, cfg.githubToken, cfg.repoSlug, cfg.organization,
|
||||||
cfg.allowedTeamSlugs);
|
cfg.allowedTeamSlugs, cfg.trustedPrLabel);
|
||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(() => bv = createBuildVerifier());
|
beforeEach(() => bv = createBuildVerifier());
|
||||||
@ -28,12 +29,13 @@ describe('BuildVerifier', () => {
|
|||||||
|
|
||||||
describe('constructor()', () => {
|
describe('constructor()', () => {
|
||||||
|
|
||||||
['secret', 'githubToken', 'repoSlug', 'organization', 'allowedTeamSlugs'].forEach(param => {
|
['secret', 'githubToken', 'repoSlug', 'organization', 'allowedTeamSlugs', 'trustedPrLabel'].
|
||||||
it(`should throw if '${param}' is missing or empty`, () => {
|
forEach(param => {
|
||||||
expect(() => createBuildVerifier({[param]: ''})).
|
it(`should throw if '${param}' is missing or empty`, () => {
|
||||||
toThrowError(`Missing or empty required parameter '${param}'!`);
|
expect(() => createBuildVerifier({[param]: ''})).
|
||||||
|
toThrowError(`Missing or empty required parameter '${param}'!`);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
it('should throw if \'allowedTeamSlugs\' is an empty array', () => {
|
it('should throw if \'allowedTeamSlugs\' is an empty array', () => {
|
||||||
@ -44,6 +46,122 @@ describe('BuildVerifier', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
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()', () => {
|
describe('verify()', () => {
|
||||||
const pr = 9;
|
const pr = 9;
|
||||||
const defaultJwt = {
|
const defaultJwt = {
|
||||||
@ -53,22 +171,21 @@ describe('BuildVerifier', () => {
|
|||||||
'pull-request': pr,
|
'pull-request': pr,
|
||||||
'slug': defaultConfig.repoSlug,
|
'slug': defaultConfig.repoSlug,
|
||||||
};
|
};
|
||||||
let bvGetPrAuthorTeamMembership: jasmine.Spy;
|
let bvGetPrIsTrusted: jasmine.Spy;
|
||||||
|
|
||||||
// Heleprs
|
// Heleprs
|
||||||
const createAuthHeader = (partialJwt: Partial<typeof defaultJwt> = {}, secret: string = defaultConfig.secret) =>
|
const createAuthHeader = (partialJwt: Partial<typeof defaultJwt> = {}, secret: string = defaultConfig.secret) =>
|
||||||
`Token ${jwt.sign({...defaultJwt, ...partialJwt}, secret)}`;
|
`Token ${jwt.sign({...defaultJwt, ...partialJwt}, secret)}`;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
bvGetPrAuthorTeamMembership = spyOn(bv, 'getPrAuthorTeamMembership').
|
bvGetPrIsTrusted = spyOn(bv, 'getPrIsTrusted').and.returnValue(Promise.resolve(true));
|
||||||
and.returnValue(Promise.resolve({author: 'some-author', isMember: true}));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
it('should return a promise', done => {
|
it('should return a promise', done => {
|
||||||
const promise = bv.verify(pr, createAuthHeader());
|
const promise = bv.verify(pr, createAuthHeader());
|
||||||
promise.then(done); // Do not complete the test (and release the spies) synchronously
|
promise.then(done); // Do not complete the test (and release the spies) synchronously
|
||||||
// to avoid running the actual `bvGetPrAuthorTeamMembership()`.
|
// to avoid running the actual `bvGetPrIsTrusted()`.
|
||||||
|
|
||||||
expect(promise).toEqual(jasmine.any(Promise));
|
expect(promise).toEqual(jasmine.any(Promise));
|
||||||
});
|
});
|
||||||
@ -148,16 +265,16 @@ describe('BuildVerifier', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
it('should call \'getPrAuthorTeamMembership()\' if the token is valid', done => {
|
it('should call \'getPrIsTrusted()\' if the token is valid', done => {
|
||||||
bv.verify(pr, createAuthHeader()).then(() => {
|
bv.verify(pr, createAuthHeader()).then(() => {
|
||||||
expect(bvGetPrAuthorTeamMembership).toHaveBeenCalledWith(pr);
|
expect(bvGetPrIsTrusted).toHaveBeenCalledWith(pr);
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
it('should fail if \'getPrAuthorTeamMembership()\' rejects', done => {
|
it('should fail if \'getPrIsTrusted()\' rejects', done => {
|
||||||
bvGetPrAuthorTeamMembership.and.callFake(() => Promise.reject('Test'));
|
bvGetPrIsTrusted.and.callFake(() => Promise.reject('Test'));
|
||||||
bv.verify(pr, createAuthHeader()).catch(err => {
|
bv.verify(pr, createAuthHeader()).catch(err => {
|
||||||
expectToBeUploadError(err, 403, `Error while verifying upload for PR ${pr}: Test`);
|
expectToBeUploadError(err, 403, `Error while verifying upload for PR ${pr}: Test`);
|
||||||
done();
|
done();
|
||||||
@ -165,97 +282,22 @@ describe('BuildVerifier', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
it('should fail if \'getPrAuthorTeamMembership()\' reports no membership', done => {
|
it('should resolve to `verifiedNotTrusted` if \'getPrIsTrusted()\' returns false', done => {
|
||||||
const errorMessage = `Error while verifying upload for PR ${pr}: User 'test' is not an active member of any of ` +
|
bvGetPrIsTrusted.and.returnValue(Promise.resolve(false));
|
||||||
'the following teams: team1, team2';
|
bv.verify(pr, createAuthHeader()).then(value => {
|
||||||
|
expect(value).toBe(BUILD_VERIFICATION_STATUS.verifiedNotTrusted);
|
||||||
bvGetPrAuthorTeamMembership.and.returnValue(Promise.resolve({author: 'test', isMember: false}));
|
|
||||||
bv.verify(pr, createAuthHeader()).catch(err => {
|
|
||||||
expectToBeUploadError(err, 403, errorMessage);
|
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
it('should succeed if everything checks outs', done => {
|
it('should resolve to `verifiedAndTrusted` if \'getPrIsTrusted()\' returns true', done => {
|
||||||
bv.verify(pr, createAuthHeader()).then(done);
|
bv.verify(pr, createAuthHeader()).then(value => {
|
||||||
});
|
expect(value).toBe(BUILD_VERIFICATION_STATUS.verifiedAndTrusted);
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
describe('getPrAuthorTeamMembership()', () => {
|
|
||||||
const pr = 9;
|
|
||||||
let prsFetchSpy: jasmine.Spy;
|
|
||||||
let teamsIsMemberBySlugSpy: jasmine.Spy;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
prsFetchSpy = spyOn(GithubPullRequests.prototype, 'fetch').
|
|
||||||
and.returnValue(Promise.resolve({user: {login: 'username'}}));
|
|
||||||
|
|
||||||
teamsIsMemberBySlugSpy = spyOn(GithubTeams.prototype, 'isMemberBySlug').
|
|
||||||
and.returnValue(Promise.resolve(true));
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
it('should return a promise', done => {
|
|
||||||
const promise = bv.getPrAuthorTeamMembership(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.getPrAuthorTeamMembership(pr).then(() => {
|
|
||||||
expect(prsFetchSpy).toHaveBeenCalledWith(pr);
|
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
it('should fail if fetching the PR errors', done => {
|
|
||||||
prsFetchSpy.and.callFake(() => Promise.reject('Test'));
|
|
||||||
bv.getPrAuthorTeamMembership(pr).catch(err => {
|
|
||||||
expect(err).toBe('Test');
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
it('should verify the PR author\'s membership in the specified teams', done => {
|
|
||||||
bv.getPrAuthorTeamMembership(pr).then(() => {
|
|
||||||
expect(teamsIsMemberBySlugSpy).toHaveBeenCalledWith('username', ['team1', 'team2']);
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
it('should fail if verifying membership errors', done => {
|
|
||||||
teamsIsMemberBySlugSpy.and.callFake(() => Promise.reject('Test'));
|
|
||||||
bv.getPrAuthorTeamMembership(pr).catch(err => {
|
|
||||||
expect(err).toBe('Test');
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
it('should return the PR\'s author and whether they are members', done => {
|
|
||||||
teamsIsMemberBySlugSpy.and.returnValues(Promise.resolve(true), Promise.resolve(false));
|
|
||||||
|
|
||||||
Promise.all([
|
|
||||||
bv.getPrAuthorTeamMembership(pr).then(({author, isMember}) => {
|
|
||||||
expect(author).toBe('username');
|
|
||||||
expect(isMember).toBe(true);
|
|
||||||
}),
|
|
||||||
bv.getPrAuthorTeamMembership(pr).then(({author, isMember}) => {
|
|
||||||
expect(author).toBe('username');
|
|
||||||
expect(isMember).toBe(false);
|
|
||||||
}),
|
|
||||||
]).then(done);
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
@ -4,8 +4,8 @@ import * as http from 'http';
|
|||||||
import * as supertest from 'supertest';
|
import * as supertest from 'supertest';
|
||||||
import {GithubPullRequests} from '../../lib/common/github-pull-requests';
|
import {GithubPullRequests} from '../../lib/common/github-pull-requests';
|
||||||
import {BuildCreator} from '../../lib/upload-server/build-creator';
|
import {BuildCreator} from '../../lib/upload-server/build-creator';
|
||||||
import {CreatedBuildEvent} from '../../lib/upload-server/build-events';
|
import {ChangedPrVisibilityEvent, CreatedBuildEvent} from '../../lib/upload-server/build-events';
|
||||||
import {BuildVerifier} from '../../lib/upload-server/build-verifier';
|
import {BUILD_VERIFICATION_STATUS, BuildVerifier} from '../../lib/upload-server/build-verifier';
|
||||||
import {uploadServerFactory as usf} from '../../lib/upload-server/upload-server-factory';
|
import {uploadServerFactory as usf} from '../../lib/upload-server/upload-server-factory';
|
||||||
|
|
||||||
// Tests
|
// Tests
|
||||||
@ -18,11 +18,12 @@ describe('uploadServerFactory', () => {
|
|||||||
githubToken: '12345',
|
githubToken: '12345',
|
||||||
repoSlug: 'repo/slug',
|
repoSlug: 'repo/slug',
|
||||||
secret: 'secret',
|
secret: 'secret',
|
||||||
|
trustedPrLabel: 'trusted: pr-label',
|
||||||
};
|
};
|
||||||
|
|
||||||
// Helpers
|
// Helpers
|
||||||
const createUploadServer = (partialConfig: Partial<typeof defaultConfig> = {}) =>
|
const createUploadServer = (partialConfig: Partial<typeof defaultConfig> = {}) =>
|
||||||
usf.create({...defaultConfig, ...partialConfig});
|
usf.create({...defaultConfig, ...partialConfig} as typeof defaultConfig);
|
||||||
|
|
||||||
|
|
||||||
describe('create()', () => {
|
describe('create()', () => {
|
||||||
@ -75,6 +76,12 @@ describe('uploadServerFactory', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
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', () => {
|
it('should return an http.Server', () => {
|
||||||
const httpCreateServerSpy = spyOn(http, 'createServer').and.callThrough();
|
const httpCreateServerSpy = spyOn(http, 'createServer').and.callThrough();
|
||||||
const server = createUploadServer();
|
const server = createUploadServer();
|
||||||
@ -141,26 +148,71 @@ describe('uploadServerFactory', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
it('should post a comment on GitHub on \'build.created\'', () => {
|
describe('on \'build.created\'', () => {
|
||||||
const prsAddCommentSpy = spyOn(GithubPullRequests.prototype, 'addComment');
|
let prsAddCommentSpy: jasmine.Spy;
|
||||||
const commentBody = 'The angular.io preview for 1234567890 is available [here][1].\n\n' +
|
|
||||||
'[1]: https://pr42-1234567890.domain.name/';
|
|
||||||
|
|
||||||
buildCreator.emit(CreatedBuildEvent.type, {pr: 42, sha: '1234567890'});
|
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();
|
||||||
|
});
|
||||||
|
|
||||||
expect(prsAddCommentSpy).toHaveBeenCalledWith(42, commentBody);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
it('should pass the correct \'githubToken\' and \'repoSlug\' to GithubPullRequests', () => {
|
it('should pass the correct \'githubToken\' and \'repoSlug\' to GithubPullRequests', () => {
|
||||||
const prsAddCommentSpy = spyOn(GithubPullRequests.prototype, 'addComment');
|
const prsAddCommentSpy = spyOn(GithubPullRequests.prototype, 'addComment');
|
||||||
|
|
||||||
buildCreator.emit(CreatedBuildEvent.type, {pr: 42, sha: '1234567890'});
|
buildCreator.emit(CreatedBuildEvent.type, {pr: 42, sha: '1234567890', isPublic: true});
|
||||||
const prs = prsAddCommentSpy.calls.mostRecent().object;
|
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).toEqual(jasmine.any(GithubPullRequests));
|
||||||
expect((prs as any).repoSlug).toBe('repo/slug');
|
expect(prs.repoSlug).toBe('repo/slug');
|
||||||
expect((prs as any).requestHeaders.Authorization).toContain('12345');
|
expect(prs.requestHeaders.Authorization).toContain('12345');
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
@ -184,6 +236,7 @@ describe('uploadServerFactory', () => {
|
|||||||
defaultConfig.repoSlug,
|
defaultConfig.repoSlug,
|
||||||
defaultConfig.githubOrganization,
|
defaultConfig.githubOrganization,
|
||||||
defaultConfig.githubTeamSlugs,
|
defaultConfig.githubTeamSlugs,
|
||||||
|
defaultConfig.trustedPrLabel,
|
||||||
);
|
);
|
||||||
buildCreator = new BuildCreator(defaultConfig.buildsDir);
|
buildCreator = new BuildCreator(defaultConfig.buildsDir);
|
||||||
agent = supertest.agent((usf as any).createMiddleware(buildVerifier, buildCreator));
|
agent = supertest.agent((usf as any).createMiddleware(buildVerifier, buildCreator));
|
||||||
@ -199,7 +252,8 @@ describe('uploadServerFactory', () => {
|
|||||||
let buildCreatorCreateSpy: jasmine.Spy;
|
let buildCreatorCreateSpy: jasmine.Spy;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
buildVerifierVerifySpy = spyOn(buildVerifier, 'verify').and.returnValue(Promise.resolve());
|
const verStatus = BUILD_VERIFICATION_STATUS.verifiedAndTrusted;
|
||||||
|
buildVerifierVerifySpy = spyOn(buildVerifier, 'verify').and.returnValue(Promise.resolve(verStatus));
|
||||||
buildCreatorCreateSpy = spyOn(buildCreator, 'create').and.returnValue(Promise.resolve());
|
buildCreatorCreateSpy = spyOn(buildCreator, 'create').and.returnValue(Promise.resolve());
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -284,14 +338,17 @@ describe('uploadServerFactory', () => {
|
|||||||
|
|
||||||
|
|
||||||
it('should call \'BuildCreator#create()\' with the correct arguments', done => {
|
it('should call \'BuildCreator#create()\' with the correct arguments', done => {
|
||||||
const req = agent.
|
buildVerifierVerifySpy.and.returnValues(
|
||||||
get(`/create-build/${pr}/${sha}`).
|
Promise.resolve(BUILD_VERIFICATION_STATUS.verifiedAndTrusted),
|
||||||
set('AUTHORIZATION', 'foo').
|
Promise.resolve(BUILD_VERIFICATION_STATUS.verifiedNotTrusted));
|
||||||
set('X-FILE', 'bar');
|
|
||||||
|
|
||||||
promisifyRequest(req).
|
const req1 = agent.get(`/create-build/${pr}/${sha}`).set('AUTHORIZATION', 'foo').set('X-FILE', 'bar');
|
||||||
then(() => expect(buildCreatorCreateSpy).toHaveBeenCalledWith(pr, sha, 'bar')).
|
const req2 = agent.get(`/create-build/${pr}/${sha}`).set('AUTHORIZATION', 'foo').set('X-FILE', 'bar');
|
||||||
then(done, done.fail);
|
|
||||||
|
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);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
@ -307,7 +364,7 @@ describe('uploadServerFactory', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
it('should respond with 201 on successful upload', done => {
|
it('should respond with 201 on successful upload (for public builds)', done => {
|
||||||
const req = agent.
|
const req = agent.
|
||||||
get(`/create-build/${pr}/${sha}`).
|
get(`/create-build/${pr}/${sha}`).
|
||||||
set('AUTHORIZATION', 'foo').
|
set('AUTHORIZATION', 'foo').
|
||||||
@ -318,6 +375,18 @@ describe('uploadServerFactory', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
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 => {
|
it('should reject PRs with leading zeros', done => {
|
||||||
verifyRequests([agent.get(`/create-build/0${pr}/${sha}`).expect(404)], done);
|
verifyRequests([agent.get(`/create-build/0${pr}/${sha}`).expect(404)], done);
|
||||||
});
|
});
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -1,5 +1,5 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
set -e -o pipefail
|
set -eu -o pipefail
|
||||||
|
|
||||||
# Set up env variables
|
# Set up env variables
|
||||||
export AIO_GITHUB_TOKEN=$(head -c -1 /aio-secrets/GITHUB_TOKEN 2>/dev/null)
|
export AIO_GITHUB_TOKEN=$(head -c -1 /aio-secrets/GITHUB_TOKEN 2>/dev/null)
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
set +e -o pipefail
|
# Using `+e` so that all checks are run and we get a complete report (even if some checks failed).
|
||||||
|
set +e -u -o pipefail
|
||||||
|
|
||||||
|
|
||||||
# Variables
|
# Variables
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
set -e -o pipefail
|
set -eu -o pipefail
|
||||||
|
|
||||||
exec >> /var/log/aio/init.log
|
exec >> /var/log/aio/init.log
|
||||||
exec 2>&1
|
exec 2>&1
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
set -e -o pipefail
|
set -eu -o pipefail
|
||||||
|
|
||||||
# Set up env variables for production
|
# Set up env variables for production
|
||||||
export AIO_GITHUB_TOKEN=$(head -c -1 /aio-secrets/GITHUB_TOKEN 2>/dev/null)
|
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)
|
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
|
# Start the upload-server instance
|
||||||
# TODO(gkalpak): Ideally, the upload server should be run as a non-privileged user.
|
# TODO(gkalpak): Ideally, the upload server should be run as a non-privileged user.
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
set -e -o pipefail
|
set -eu -o pipefail
|
||||||
|
|
||||||
# Set up env variables for testing
|
# Set up env variables for testing
|
||||||
export AIO_BUILDS_DIR=$TEST_AIO_BUILDS_DIR
|
export AIO_BUILDS_DIR=$TEST_AIO_BUILDS_DIR
|
||||||
export AIO_DOMAIN_NAME=$TEST_AIO_DOMAIN_NAME
|
export AIO_DOMAIN_NAME=$TEST_AIO_DOMAIN_NAME
|
||||||
export AIO_GITHUB_ORGANIZATION=$TEST_AIO_GITHUB_ORGANIZATION
|
export AIO_GITHUB_ORGANIZATION=$TEST_AIO_GITHUB_ORGANIZATION
|
||||||
export AIO_GITHUB_TEAM_SLUGS=$TEST_AIO_GITHUB_TEAM_SLUGS
|
export AIO_GITHUB_TEAM_SLUGS=$TEST_AIO_GITHUB_TEAM_SLUGS
|
||||||
export AIO_PREVIEW_DEPLOYMENT_TOKEN=$TEST_AIO_PREVIEW_DEPLOYMENT_TOKEN
|
|
||||||
export AIO_REPO_SLUG=$TEST_AIO_REPO_SLUG
|
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_HOSTNAME=$TEST_AIO_UPLOAD_HOSTNAME
|
||||||
export AIO_UPLOAD_PORT=$TEST_AIO_UPLOAD_PORT
|
export AIO_UPLOAD_PORT=$TEST_AIO_UPLOAD_PORT
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
set -e -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
|
uploadServerLogFile=/var/log/aio/upload-server-verify-setup.log
|
||||||
|
@ -4,7 +4,8 @@
|
|||||||
## Overview
|
## Overview
|
||||||
- [General overview](overview--general.md)
|
- [General overview](overview--general.md)
|
||||||
- [Security model](overview--security-model.md)
|
- [Security model](overview--security-model.md)
|
||||||
- [Available Commands](overview--scripts-and-commands.md)
|
- [Available scripts and commands](overview--scripts-and-commands.md)
|
||||||
|
- [HTTP status codes](overview--http-status-codes.md)
|
||||||
|
|
||||||
|
|
||||||
## Setting up the VM
|
## Setting up the VM
|
||||||
|
@ -17,7 +17,7 @@ you don't need to specify values for those.
|
|||||||
The domain name of the server.
|
The domain name of the server.
|
||||||
|
|
||||||
- `AIO_GITHUB_ORGANIZATION`:
|
- `AIO_GITHUB_ORGANIZATION`:
|
||||||
The GitHub organization whose teams arew whitelisted for accepting uploads.
|
The GitHub organization whose teams are whitelisted for accepting uploads.
|
||||||
See also `AIO_GITHUB_TEAM_SLUGS`.
|
See also `AIO_GITHUB_TEAM_SLUGS`.
|
||||||
|
|
||||||
- `AIO_GITHUB_TEAM_SLUGS`:
|
- `AIO_GITHUB_TEAM_SLUGS`:
|
||||||
@ -39,6 +39,11 @@ you don't need to specify values for those.
|
|||||||
- `AIO_REPO_SLUG`:
|
- `AIO_REPO_SLUG`:
|
||||||
The repository slug (in the form `<user>/<repo>`) for which PRs will be uploaded.
|
The repository slug (in the form `<user>/<repo>`) for which PRs will be uploaded.
|
||||||
|
|
||||||
|
- `AIO_TRUSTED_PR_LABEL`:
|
||||||
|
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
|
||||||
|
from trusted authors).
|
||||||
|
|
||||||
- `AIO_UPLOAD_HOSTNAME`:
|
- `AIO_UPLOAD_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 upload-server. This is used by nginx for
|
||||||
delegating upload requests and also for performing a periodic health-check.
|
delegating upload requests and also for performing a periodic health-check.
|
||||||
|
@ -33,36 +33,51 @@ container:
|
|||||||
|
|
||||||
|
|
||||||
### On CI (Travis)
|
### On CI (Travis)
|
||||||
- Build job completes successfully (i.e. build succeeds and tests pass).
|
- 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 inside the angular.io project directory
|
- The CI script checks whether the PR has touched any files that might affect the angular.io app
|
||||||
(currently `aio/`).
|
(currently the `aio/` or `packages/` directories, ignoring spec files).
|
||||||
- The CI script checks whether the author of the PR is a member of one of the whitelisted GitHub
|
- Optionally, the CI script can check whether the PR can be automatically verified (i.e. if the
|
||||||
teams (and therefore allowed to upload).
|
author of the PR is a member of one of the whitelisted GitHub teams or the PR has the specified
|
||||||
|
"trusted PR" label).
|
||||||
**Note:**
|
**Note:**
|
||||||
For security reasons, the same checks will be performed on the server as well. This is an optional
|
For security reasons, the same checks will be performed on the server as well. This is an optional
|
||||||
step with the purpose of:
|
step that can be used in case one wants to apply special logic depending on the outcome of the
|
||||||
1. Avoiding the wasted overhead associated with uploads that are going to be rejected (e.g.
|
pre-verification. For example:
|
||||||
building the artifacts, sending them to the server, running checks on the server, etc).
|
1. One might want to deploy automatically verified PRs only. In that case, the pre-verification
|
||||||
2. Avoiding failing the build (due to an error response from the server) or requiring additional
|
helps avoid the wasted overhead associated with uploads that are going to be rejected (e.g.
|
||||||
logic for detecting the reasons of the failure.
|
building the artifacts, sending them to the server, running checks on the server, detecting the
|
||||||
- The CI script gzip and upload the build artifacts to the server.
|
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
|
### Uploading build artifacts
|
||||||
- nginx receives upload request.
|
- nginx receives the upload request.
|
||||||
- nginx checks that the uploaded gzip archive does not exceed the specified max file size, stores it
|
- nginx checks that the uploaded gzip archive does not exceed the specified max file size, stores it
|
||||||
in a temporary location and passes the filepath to the Node.js upload-server.
|
in a temporary location and passes the filepath to the Node.js upload-server.
|
||||||
- The upload-server verifies that the uploaded file is not trying to overwrite an existing build,
|
- The upload-server runs several checks to determine whether the request should be accepted and
|
||||||
and runs several checks to determine whether the request should be accepted (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 deploys the artifacts to a sub-directory named after the PR number and SHA:
|
- The upload-server changes the "visibility" of the associated PR, if necessary. For example, if
|
||||||
`<PR>/<SHA>/`
|
builds for the same PR had been previously deployed as non-public and the current build has been
|
||||||
- The upload-server posts a comment on the corresponding PR on GitHub mentioning the SHA and the
|
automatically verified, all previous builds are made public as well.
|
||||||
the link where the preview can be found.
|
If the PR transitions from "non-public" to "public", the upload-server posts a comment on the
|
||||||
|
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 upload-server deploys the artifacts to a sub-directory named after the PR number and the first
|
||||||
|
few characters of the SHA: `<PR>/<SHA>/`
|
||||||
|
(Non-publicly accessible PRs will be stored in a different location, but again derived from the PR
|
||||||
|
number and SHA.)
|
||||||
|
- If the PR is publicly accessible, the upload-server posts a comment on the corresponding PR on
|
||||||
|
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
|
||||||
|
[here](overview--http-status-codes.md).
|
||||||
|
|
||||||
|
|
||||||
### Serving build artifacts
|
### Serving build artifacts
|
||||||
@ -71,6 +86,9 @@ More info on how to set things up on CI can be found [here](misc--integrate-with
|
|||||||
- 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`
|
||||||
|
|
||||||
|
Again, more info on the possible HTTP status codes and their meaning can be found
|
||||||
|
[here](overview--http-status-codes.md).
|
||||||
|
|
||||||
|
|
||||||
### Removing obsolete artifacts
|
### Removing obsolete artifacts
|
||||||
In order to avoid flooding the disk with unnecessary build artifacts, there is a cronjob that runs a
|
In order to avoid flooding the disk with unnecessary build artifacts, there is a cronjob that runs a
|
||||||
|
66
aio/aio-builds-setup/docs/overview--http-status-codes.md
Normal file
66
aio/aio-builds-setup/docs/overview--http-status-codes.md
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
# Overview - HTTP Status Codes
|
||||||
|
|
||||||
|
|
||||||
|
This is a list of all the possible HTTP status codes returned by the nginx anf upload servers, along
|
||||||
|
with a bried explanation of what they mean:
|
||||||
|
|
||||||
|
|
||||||
|
## `http://*.ngbuilds.io/*`
|
||||||
|
|
||||||
|
- **307 (Temporary Redirect)**:
|
||||||
|
All non-HTTPS requests. 308 (Permanent Redirect) would be more appropriate, but is not supported
|
||||||
|
by all agents (e.g. cURL).
|
||||||
|
|
||||||
|
|
||||||
|
## `https://pr<pr>-<sha>.ngbuilds.io/*`
|
||||||
|
|
||||||
|
- **200 (OK)**:
|
||||||
|
File was found or URL was rewritten to `/index.html` (i.e. all paths that have no `.` in final
|
||||||
|
segment).
|
||||||
|
|
||||||
|
- **403 (Forbidden)**:
|
||||||
|
Trying to access a sub-directory.
|
||||||
|
|
||||||
|
- **404 (Not Found)**:
|
||||||
|
File not found.
|
||||||
|
|
||||||
|
|
||||||
|
## `https://ngbuilds.io/create-build/<pr>/<sha>`
|
||||||
|
|
||||||
|
- **201 (Created)**:
|
||||||
|
Build deployed successfully and is publicly available.
|
||||||
|
|
||||||
|
- **202 (Accepted)**:
|
||||||
|
Build not automatically verifiable. Stored for later deployment (after re-verification).
|
||||||
|
|
||||||
|
- **400 (Bad Request)**:
|
||||||
|
No payload.
|
||||||
|
|
||||||
|
- **401 (Unauthorized)**:
|
||||||
|
No `AUTHORIZATION` header.
|
||||||
|
|
||||||
|
- **403 (Forbidden)**:
|
||||||
|
Unable to verify build (e.g. invalid JWT token, or unable to talk to 3rd-party APIs, etc).
|
||||||
|
|
||||||
|
- **404 (Not Found)**:
|
||||||
|
Tried to change PR visibility but the source directory did not exist.
|
||||||
|
(Currently, this can only happen as a rare race condition during build deployment.)
|
||||||
|
|
||||||
|
- **405 (Method Not Allowed)**:
|
||||||
|
Request method other than POST.
|
||||||
|
|
||||||
|
- **409 (Conflict)**:
|
||||||
|
Request to overwrite existing directory (e.g. deploy existing build or 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/*`
|
||||||
|
|
||||||
|
- **404 (Not Found)**:
|
||||||
|
Request not matched by the above rules.
|
||||||
|
|
||||||
|
- **500 (Internal Server Error)**:
|
||||||
|
Error while processing a request matched by the above rules.
|
@ -12,20 +12,22 @@ available:
|
|||||||
Can be used for creating a preconfigured docker image.
|
Can be used for creating a preconfigured docker image.
|
||||||
See [here](vm-setup--create-docker-image.md) for more info.
|
See [here](vm-setup--create-docker-image.md) for more info.
|
||||||
|
|
||||||
- `test.sh`
|
- `test.sh`:
|
||||||
Can be used for running the tests for `<aio-builds-setup-dir>/dockerbuild/scripts-js/`. This is
|
Can be used for running the tests for `<aio-builds-setup-dir>/dockerbuild/scripts-js/`. This is
|
||||||
useful for CI integration. See [here](misc--integrate-with-ci.md) for more info.
|
useful for CI integration. See [here](misc--integrate-with-ci.md) for more info.
|
||||||
|
|
||||||
- `travis-preverify-pr.sh`
|
- `travis-preverify-pr.sh`:
|
||||||
Can be used for "preverifying" a PR before uploading the artifacts to the server. It checks that
|
Can be used for "pre-verifying" a PR before uploading the artifacts to the server. It checks
|
||||||
the author of the PR is a member of one of the specified GitHub teams and therefore allowed to
|
whether the author of the PR is a member of one of the specified GitHub teams (therefore allowed
|
||||||
upload build artifacts. This is useful for CI integration. See [here](misc--integrate-with-ci.md)
|
to upload build artifacts) or the PR has the specified "trusted PR" label (meaning it has been
|
||||||
for more info.
|
manually verified by a trusted member). This is useful for CI integration.
|
||||||
|
See [here](misc--integrate-with-ci.md) for more info.
|
||||||
|
|
||||||
- `update-preview-server.sh`
|
- `update-preview-server.sh`:
|
||||||
Can be used for updating the docker container (and image) based on the latest changes checked out
|
Can be used for updating the docker container (and image) based on the latest changes checked out
|
||||||
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
|
## 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
|
||||||
|
@ -41,12 +41,13 @@ part of the CI setup and serving them publicly.
|
|||||||
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.
|
1. Verify which PR the uploaded artifacts correspond to.
|
||||||
2. Determine the author of the PR.
|
2. Fetch the PR's metadata, including author and labels.
|
||||||
3. Check whether the PR author is a member of some whitelisted GitHub team.
|
3. Check whether the PR can be automatically verified as "trusted" (based on its author or labels).
|
||||||
4. Deploy the artifacts to the corresponding PR's directory.
|
4. If necessary, update the corresponding PR's verification status.
|
||||||
5. Prevent overwriting previously deployed artifacts (which ensures that the guarantees established
|
5. Deploy the artifacts to the corresponding PR's directory.
|
||||||
|
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).
|
||||||
6. Prevent uploaded files from accessing anything outside their directory.
|
7. Prevent uploaded files from accessing anything outside their directory.
|
||||||
|
|
||||||
|
|
||||||
### Implementation details
|
### Implementation details
|
||||||
@ -65,35 +66,51 @@ This section describes how each of the aforementioned sub-tasks is accomplished:
|
|||||||
_There are currently certain limitation in the implementation of the JWT addon._
|
_There are currently certain limitation in the implementation of the JWT addon._
|
||||||
_See the next section for more details._
|
_See the next section for more details._
|
||||||
|
|
||||||
2. **Determine the author of the PR.**
|
2. **Fetch the PR's metadata, including author and labels**.
|
||||||
|
|
||||||
Once we have securely associated the uploaded artifaacts to a PR, we retrieve the PR's metadata -
|
Once we have securely associated the uploaded artifacts to a PR, we retrieve the PR's metadata -
|
||||||
including the author's username - using the [GitHub API](https://developer.github.com/v3/).
|
including the author's username and the labels - using the
|
||||||
|
[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
|
||||||
[@mary-poppins](https://github.com/mary-poppins)).
|
[@mary-poppins](https://github.com/mary-poppins)).
|
||||||
|
|
||||||
3. **Check whether the PR author is a member of some whitelisted GitHub team.**
|
3. **Check whether the PR can be automatically verified as "trusted"**.
|
||||||
|
|
||||||
Again using the GitHub API, we can verify the author's membership in one of the
|
"Trusted" means that we are confident that the build artifacts are suitable for being deployed
|
||||||
whitelisted/trusted GitHub teams. For this operation, we need a PErsonal Access Token with the
|
and publicly accessible on the preview server. There are two ways to check that:
|
||||||
`read:org` scope issued by a user that can "see" the specified GitHub organization.
|
1. We can verify that the PR has a pre-determined label, which marks it as "safe for preview".
|
||||||
Here too, we use token by @mary-poppins.
|
Such a label can only have been added by a maintainer (with the necessary rights) and
|
||||||
|
designates that they have manually verified the PR contents.
|
||||||
|
2. We can verify (again using the GitHub API) the author's membership in one of the
|
||||||
|
whitelisted/trusted GitHub teams. For this operation, we need a Personal Access Token with the
|
||||||
|
`read:org` scope issued by a user that can "see" the specified GitHub organization.
|
||||||
|
Here too, we use the token by @mary-poppins.
|
||||||
|
|
||||||
4. **Deploy the artifacts to the corresponding PR's directory.**
|
4. **If necessary update the corresponding PR's verification status**.
|
||||||
|
|
||||||
With the preceeding steps, we have verified that the uploaded artifacts have been uploaded by
|
Once we have determined whether the PR is considered "trusted", we update its "visibility" (i.e.
|
||||||
Travis and correspond to a PR whose author is a member of a trusted team. Essentially, as long as
|
whether it is publicly accessible or not), based on the new verification status. For example, if
|
||||||
sub-tasks 1, 2 and 3 can be securely accomplished, it is possible to "project" the trust we have
|
a PR was initially considered "not trusted" but the check triggered by a new build determined
|
||||||
in a team's members through the PR and Travis to the build artifacts.
|
otherwise, the PR (and all the previously uploaded previews) are made public. It works the same
|
||||||
|
way if a PR has gone from "trusted" to "not trusted".
|
||||||
|
|
||||||
5. **Prevent overwriting previously deployed artifacts**.
|
5. **Deploy the artifacts to the corresponding PR's directory.**
|
||||||
|
|
||||||
In order to enforce this restriction (and ensure that the deployed artifacts validity is
|
With the preceding steps, we have verified that the uploaded artifacts have been uploaded by
|
||||||
|
Travis. 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
|
||||||
|
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
|
||||||
|
"project" the trust we have in a team's members through the PR and Travis to the build artifacts.
|
||||||
|
|
||||||
|
6. **Prevent overwriting previously deployed artifacts**.
|
||||||
|
|
||||||
|
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 upload (currently a Node.js
|
||||||
Express server) rejects uploads that target an existing directory.
|
Express server) rejects uploads that target an existing directory.
|
||||||
_Note: A PR can contain multiple uploads; one for each SHA that was built on Travis._
|
_Note: A PR can contain multiple uploads; one for each SHA that was built on Travis._
|
||||||
|
|
||||||
6. **Prevent uploaded files from accessing anything outside their directory.**
|
7. **Prevent uploaded 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 uploaded artifacts) has been configured to not follow symlinks
|
||||||
outside of the directory where the build artifacts are stored.
|
outside of the directory where the build artifacts are stored.
|
||||||
@ -104,6 +121,11 @@ This section describes how each of the aforementioned sub-tasks is accomplished:
|
|||||||
- Each trusted PR author has full control over the content that is uploaded for their PRs. Part of
|
- Each trusted PR author has full control over the content that is uploaded for their PRs. Part of
|
||||||
the security model relies on the trustworthiness of these authors.
|
the security model relies on the trustworthiness of these authors.
|
||||||
|
|
||||||
|
- Adding the specified label on a PR and marking it as trusted, gives the author full control over
|
||||||
|
the content that is uploaded for the specific PR (e.g. by pushing more commits to it). The user
|
||||||
|
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
|
- If anyone gets access to the `PREVIEW_DEPLOYMENT_TOKEN` (a.k.a. `NGBUILDS_IO_KEY` on
|
||||||
angular/angular) variable generated for each Travis job, they will be able to impersonate the
|
angular/angular) variable generated for each Travis job, they will be able to impersonate the
|
||||||
corresponding PR's author on the preview server for as long as the token is valid (currently 90
|
corresponding PR's author on the preview server for as long as the token is valid (currently 90
|
||||||
|
@ -25,7 +25,7 @@ The following commands would create a docker image from GitHub repo `foo/bar` to
|
|||||||
--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 \
|
||||||
--build-arg AIO_GITHUB_TEMA_SLUGS=bar-core,bar-docs-authors
|
--build-arg AIO_GITHUB_TEAM_SLUGS=bar-core,bar-docs-authors
|
||||||
```
|
```
|
||||||
|
|
||||||
A full list of the available environment variables can be found
|
A full list of the available environment variables can be found
|
||||||
|
@ -5,6 +5,14 @@ set -eux -o pipefail
|
|||||||
source "`dirname $0`/_env.sh"
|
source "`dirname $0`/_env.sh"
|
||||||
readonly defaultImageNameAndTag="aio-builds:latest"
|
readonly defaultImageNameAndTag="aio-builds:latest"
|
||||||
|
|
||||||
|
# Build `scripts-js/`
|
||||||
|
# (Necessary, because only `scripts-js/dist/` is copied to the docker image.)
|
||||||
|
(
|
||||||
|
cd "$SCRIPTS_JS_DIR"
|
||||||
|
yarn install
|
||||||
|
yarn build
|
||||||
|
)
|
||||||
|
|
||||||
# Create docker image
|
# Create docker image
|
||||||
readonly nameAndOptionalTag=${1:-$defaultImageNameAndTag}
|
readonly nameAndOptionalTag=${1:-$defaultImageNameAndTag}
|
||||||
sudo docker build --tag $nameAndOptionalTag ${@:2} $DOCKERBUILD_DIR
|
sudo docker build --tag $nameAndOptionalTag ${@:2} $DOCKERBUILD_DIR
|
||||||
|
@ -16,5 +16,11 @@ AIO_GITHUB_ORGANIZATION="angular" \
|
|||||||
AIO_GITHUB_TEAM_SLUGS="angular-core,aio-contributors" \
|
AIO_GITHUB_TEAM_SLUGS="angular-core,aio-contributors" \
|
||||||
AIO_GITHUB_TOKEN=$(echo ${GITHUB_TEAM_MEMBERSHIP_CHECK_KEY} | rev) \
|
AIO_GITHUB_TOKEN=$(echo ${GITHUB_TEAM_MEMBERSHIP_CHECK_KEY} | rev) \
|
||||||
AIO_REPO_SLUG=$TRAVIS_REPO_SLUG \
|
AIO_REPO_SLUG=$TRAVIS_REPO_SLUG \
|
||||||
|
AIO_TRUSTED_PR_LABEL="aio: preview" \
|
||||||
AIO_PREVERIFY_PR=$TRAVIS_PULL_REQUEST \
|
AIO_PREVERIFY_PR=$TRAVIS_PULL_REQUEST \
|
||||||
node "$SCRIPTS_JS_DIR/dist/lib/upload-server/index-preverify-pr"
|
node "$SCRIPTS_JS_DIR/dist/lib/upload-server/index-preverify-pr"
|
||||||
|
|
||||||
|
# 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.
|
||||||
|
@ -14,13 +14,13 @@
|
|||||||
<h1>Example Snippets</h1>
|
<h1>Example Snippets</h1>
|
||||||
|
|
||||||
<!-- #docregion ngClass -->
|
<!-- #docregion ngClass -->
|
||||||
<div [ngClass]="{active: isActive}">
|
<div [ngClass]="{'active': isActive}">
|
||||||
<!-- #enddocregion ngClass -->
|
<!-- #enddocregion ngClass -->
|
||||||
[ngClass] active
|
[ngClass] active
|
||||||
</div>
|
</div>
|
||||||
<!-- #docregion ngClass -->
|
<!-- #docregion ngClass -->
|
||||||
<div [ngClass]="{active: isActive,
|
<div [ngClass]="{'active': isActive,
|
||||||
shazam: isImportant}">
|
'shazam': isImportant}">
|
||||||
<!-- #enddocregion ngClass -->
|
<!-- #enddocregion ngClass -->
|
||||||
[ngClass] active and boldly important
|
[ngClass] active and boldly important
|
||||||
</div>
|
</div>
|
||||||
@ -57,7 +57,7 @@
|
|||||||
|
|
||||||
<p></p>
|
<p></p>
|
||||||
<!-- #docregion ngStyle -->
|
<!-- #docregion ngStyle -->
|
||||||
<div [ngStyle]="{color: colorPreference}">
|
<div [ngStyle]="{'color': colorPreference}">
|
||||||
<!-- #enddocregion ngStyle -->
|
<!-- #enddocregion ngStyle -->
|
||||||
color preference #1
|
color preference #1
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,8 +1,7 @@
|
|||||||
// #docregion
|
// #docregion
|
||||||
import rollup from 'rollup'
|
import nodeResolve from 'rollup-plugin-node-resolve';
|
||||||
import nodeResolve from 'rollup-plugin-node-resolve'
|
|
||||||
import commonjs from 'rollup-plugin-commonjs';
|
import commonjs from 'rollup-plugin-commonjs';
|
||||||
import uglify from 'rollup-plugin-uglify'
|
import uglify from 'rollup-plugin-uglify';
|
||||||
|
|
||||||
// #docregion config
|
// #docregion config
|
||||||
export default {
|
export default {
|
||||||
@ -30,5 +29,5 @@ export default {
|
|||||||
uglify()
|
uglify()
|
||||||
// #enddocregion uglify
|
// #enddocregion uglify
|
||||||
]
|
]
|
||||||
}
|
};
|
||||||
// #enddocregion config
|
// #enddocregion config
|
||||||
|
@ -6,7 +6,7 @@ import { Hero } from './hero';
|
|||||||
const HEROES = [
|
const HEROES = [
|
||||||
new Hero('Windstorm', 'Weather mastery'),
|
new Hero('Windstorm', 'Weather mastery'),
|
||||||
new Hero('Mr. Nice', 'Killing them with kindness'),
|
new Hero('Mr. Nice', 'Killing them with kindness'),
|
||||||
new Hero('Magneta', 'Manipulates metalic objects')
|
new Hero('Magneta', 'Manipulates metallic objects')
|
||||||
];
|
];
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
|
@ -9,6 +9,6 @@ describe('cli-quickstart App', () => {
|
|||||||
|
|
||||||
it('should display message saying app works', () => {
|
it('should display message saying app works', () => {
|
||||||
let pageTitle = element(by.css('app-root h1')).getText();
|
let pageTitle = element(by.css('app-root h1')).getText();
|
||||||
expect(pageTitle).toEqual('My First Angular App');
|
expect(pageTitle).toEqual('Welcome to My First Angular App!!');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -9,6 +9,6 @@ describe('my-app App', function() {
|
|||||||
|
|
||||||
it('should display message saying app works', () => {
|
it('should display message saying app works', () => {
|
||||||
page.navigateTo();
|
page.navigateTo();
|
||||||
expect(page.getParagraphText()).toEqual('app works!');
|
expect(page.getParagraphText()).toEqual('Welcome to app!!');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,18 +1,12 @@
|
|||||||
{
|
{
|
||||||
|
"extends": "../tsconfig.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"sourceMap": true,
|
"outDir": "../out-tsc/e2e",
|
||||||
"declaration": false,
|
|
||||||
"moduleResolution": "node",
|
|
||||||
"emitDecoratorMetadata": true,
|
|
||||||
"experimentalDecorators": true,
|
|
||||||
"lib": [
|
|
||||||
"es2016"
|
|
||||||
],
|
|
||||||
"outDir": "../dist/out-tsc-e2e",
|
|
||||||
"module": "commonjs",
|
"module": "commonjs",
|
||||||
"target": "es6",
|
"target": "es5",
|
||||||
"types":[
|
"types": [
|
||||||
"jasmine",
|
"jasmine",
|
||||||
|
"jasminewd2",
|
||||||
"node"
|
"node"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,20 @@
|
|||||||
<h1>
|
<!--The content below is only a placeholder and can be replaced.-->
|
||||||
{{title}}
|
<div style="text-align:center">
|
||||||
</h1>
|
<h1>
|
||||||
|
Welcome to {{title}}!!
|
||||||
|
</h1>
|
||||||
|
<img width="300" alt="Angular logo" src="data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4NCjwhLS0gR2VuZXJhdG9yOiBBZG9iZSBJbGx1c3RyYXRvciAxOS4xLjAsIFNWRyBFeHBvcnQgUGx1Zy1JbiAuIFNWRyBWZXJzaW9uOiA2LjAwIEJ1aWxkIDApICAtLT4NCjxzdmcgdmVyc2lvbj0iMS4xIiBpZD0iTGF5ZXJfMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgeD0iMHB4IiB5PSIwcHgiDQoJIHZpZXdCb3g9IjAgMCAyNTAgMjUwIiBzdHlsZT0iZW5hYmxlLWJhY2tncm91bmQ6bmV3IDAgMCAyNTAgMjUwOyIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+DQo8c3R5bGUgdHlwZT0idGV4dC9jc3MiPg0KCS5zdDB7ZmlsbDojREQwMDMxO30NCgkuc3Qxe2ZpbGw6I0MzMDAyRjt9DQoJLnN0MntmaWxsOiNGRkZGRkY7fQ0KPC9zdHlsZT4NCjxnPg0KCTxwb2x5Z29uIGNsYXNzPSJzdDAiIHBvaW50cz0iMTI1LDMwIDEyNSwzMCAxMjUsMzAgMzEuOSw2My4yIDQ2LjEsMTg2LjMgMTI1LDIzMCAxMjUsMjMwIDEyNSwyMzAgMjAzLjksMTg2LjMgMjE4LjEsNjMuMiAJIi8+DQoJPHBvbHlnb24gY2xhc3M9InN0MSIgcG9pbnRzPSIxMjUsMzAgMTI1LDUyLjIgMTI1LDUyLjEgMTI1LDE1My40IDEyNSwxNTMuNCAxMjUsMjMwIDEyNSwyMzAgMjAzLjksMTg2LjMgMjE4LjEsNjMuMiAxMjUsMzAgCSIvPg0KCTxwYXRoIGNsYXNzPSJzdDIiIGQ9Ik0xMjUsNTIuMUw2Ni44LDE4Mi42aDBoMjEuN2gwbDExLjctMjkuMmg0OS40bDExLjcsMjkuMmgwaDIxLjdoMEwxMjUsNTIuMUwxMjUsNTIuMUwxMjUsNTIuMUwxMjUsNTIuMQ0KCQlMMTI1LDUyLjF6IE0xNDIsMTM1LjRIMTA4bDE3LTQwLjlMMTQyLDEzNS40eiIvPg0KPC9nPg0KPC9zdmc+DQo=">
|
||||||
|
</div>
|
||||||
|
<h2>Here are some links to help you start: </h2>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<h2><a target="_blank" href="https://angular.io/tutorial">Tour of Heroes</a></h2>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<h2><a target="_blank" href="https://github.com/angular/angular-cli/wiki">CLI Documentation</a></h2>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<h2><a target="_blank" href="http://angularjs.blogspot.ca/">Angular blog</a></h2>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
@ -17,16 +17,16 @@ describe('AppComponent', () => {
|
|||||||
expect(app).toBeTruthy();
|
expect(app).toBeTruthy();
|
||||||
}));
|
}));
|
||||||
|
|
||||||
it(`should have as title 'app works!'`, async(() => {
|
it(`should have as title 'app'`, async(() => {
|
||||||
const fixture = TestBed.createComponent(AppComponent);
|
const fixture = TestBed.createComponent(AppComponent);
|
||||||
const app = fixture.debugElement.componentInstance;
|
const app = fixture.debugElement.componentInstance;
|
||||||
expect(app.title).toEqual('app works!');
|
expect(app.title).toEqual('app');
|
||||||
}));
|
}));
|
||||||
|
|
||||||
it('should render title in a h1 tag', async(() => {
|
it('should render title in a h1 tag', async(() => {
|
||||||
const fixture = TestBed.createComponent(AppComponent);
|
const fixture = TestBed.createComponent(AppComponent);
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
const compiled = fixture.debugElement.nativeElement;
|
const compiled = fixture.debugElement.nativeElement;
|
||||||
expect(compiled.querySelector('h1').textContent).toContain('app works!');
|
expect(compiled.querySelector('h1').textContent).toContain('Welcome to app!!');
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
|
@ -1,7 +1,5 @@
|
|||||||
import { BrowserModule } from '@angular/platform-browser';
|
import { BrowserModule } from '@angular/platform-browser';
|
||||||
import { NgModule } from '@angular/core';
|
import { NgModule } from '@angular/core';
|
||||||
import { FormsModule } from '@angular/forms';
|
|
||||||
import { HttpModule } from '@angular/http';
|
|
||||||
|
|
||||||
import { AppComponent } from './app.component';
|
import { AppComponent } from './app.component';
|
||||||
|
|
||||||
@ -10,9 +8,7 @@ import { AppComponent } from './app.component';
|
|||||||
AppComponent
|
AppComponent
|
||||||
],
|
],
|
||||||
imports: [
|
imports: [
|
||||||
BrowserModule,
|
BrowserModule
|
||||||
FormsModule,
|
|
||||||
HttpModule
|
|
||||||
],
|
],
|
||||||
providers: [],
|
providers: [],
|
||||||
bootstrap: [AppComponent]
|
bootstrap: [AppComponent]
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html>
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<title>MyApp</title>
|
<title>MyApp</title>
|
||||||
@ -9,6 +9,6 @@
|
|||||||
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<app-root>Loading...</app-root>
|
<app-root></app-root>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
@ -31,21 +31,21 @@
|
|||||||
// import 'core-js/es6/array';
|
// import 'core-js/es6/array';
|
||||||
// import 'core-js/es6/regexp';
|
// import 'core-js/es6/regexp';
|
||||||
// import 'core-js/es6/map';
|
// import 'core-js/es6/map';
|
||||||
|
// import 'core-js/es6/weak-map';
|
||||||
// import 'core-js/es6/set';
|
// import 'core-js/es6/set';
|
||||||
|
|
||||||
/** IE10 and IE11 requires the following for NgClass support on SVG elements */
|
/** IE10 and IE11 requires the following for NgClass support on SVG elements */
|
||||||
// import 'classlist.js'; // Run `npm install --save classlist.js`.
|
// import 'classlist.js'; // Run `npm install --save classlist.js`.
|
||||||
|
|
||||||
/** IE10 and IE11 requires the following to support `@angular/animation`. */
|
|
||||||
// import 'web-animations-js'; // Run `npm install --save web-animations-js`.
|
|
||||||
|
|
||||||
|
|
||||||
/** Evergreen browsers require these. **/
|
/** Evergreen browsers require these. **/
|
||||||
import 'core-js/es6/reflect';
|
import 'core-js/es6/reflect';
|
||||||
import 'core-js/es7/reflect';
|
import 'core-js/es7/reflect';
|
||||||
|
|
||||||
|
|
||||||
/** ALL Firefox browsers require the following to support `@angular/animation`. **/
|
/**
|
||||||
|
* Required to support Web Animations `@angular/animation`.
|
||||||
|
* Needed for: All but Chrome, Firefox and Opera. http://caniuse.com/#feat=web-animation
|
||||||
|
**/
|
||||||
// import 'web-animations-js'; // Run `npm install --save web-animations-js`.
|
// import 'web-animations-js'; // Run `npm install --save web-animations-js`.
|
||||||
|
|
||||||
|
|
||||||
@ -66,3 +66,7 @@ import 'zone.js/dist/zone'; // Included with Angular CLI.
|
|||||||
* Needed for: All but Chrome, Firefox, Edge, IE11 and Safari 10
|
* Needed for: All but Chrome, Firefox, Edge, IE11 and Safari 10
|
||||||
*/
|
*/
|
||||||
// import 'intl'; // Run `npm install --save intl`.
|
// import 'intl'; // Run `npm install --save intl`.
|
||||||
|
/**
|
||||||
|
* Need to import at least one locale-data with intl.
|
||||||
|
*/
|
||||||
|
// import 'intl/locale-data/jsonp/en';
|
||||||
|
@ -1,16 +1,7 @@
|
|||||||
{
|
{
|
||||||
|
"extends": "../tsconfig.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"sourceMap": true,
|
|
||||||
"declaration": false,
|
|
||||||
"moduleResolution": "node",
|
|
||||||
"emitDecoratorMetadata": true,
|
|
||||||
"experimentalDecorators": true,
|
|
||||||
"lib": [
|
|
||||||
"es2016",
|
|
||||||
"dom"
|
|
||||||
],
|
|
||||||
"outDir": "../out-tsc/app",
|
"outDir": "../out-tsc/app",
|
||||||
"target": "es5",
|
|
||||||
"module": "es2015",
|
"module": "es2015",
|
||||||
"baseUrl": "",
|
"baseUrl": "",
|
||||||
"types": []
|
"types": []
|
||||||
|
@ -1,16 +1,9 @@
|
|||||||
{
|
{
|
||||||
|
"extends": "../tsconfig.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"sourceMap": true,
|
|
||||||
"declaration": false,
|
|
||||||
"moduleResolution": "node",
|
|
||||||
"emitDecoratorMetadata": true,
|
|
||||||
"experimentalDecorators": true,
|
|
||||||
"lib": [
|
|
||||||
"es2016"
|
|
||||||
],
|
|
||||||
"outDir": "../out-tsc/spec",
|
"outDir": "../out-tsc/spec",
|
||||||
"module": "commonjs",
|
"module": "commonjs",
|
||||||
"target": "es6",
|
"target": "es5",
|
||||||
"baseUrl": "",
|
"baseUrl": "",
|
||||||
"types": [
|
"types": [
|
||||||
"jasmine",
|
"jasmine",
|
||||||
@ -21,6 +14,7 @@
|
|||||||
"test.ts"
|
"test.ts"
|
||||||
],
|
],
|
||||||
"include": [
|
"include": [
|
||||||
"**/*.spec.ts"
|
"**/*.spec.ts",
|
||||||
|
"**/*.d.ts"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
5
aio/content/examples/cli-quickstart/src/typings.d.ts
vendored
Normal file
5
aio/content/examples/cli-quickstart/src/typings.d.ts
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
/* SystemJS module definition */
|
||||||
|
declare var module: NodeModule;
|
||||||
|
interface NodeModule {
|
||||||
|
id: string;
|
||||||
|
}
|
@ -2,13 +2,19 @@
|
|||||||
"compileOnSave": false,
|
"compileOnSave": false,
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"outDir": "./dist/out-tsc",
|
"outDir": "./dist/out-tsc",
|
||||||
|
"baseUrl": "src",
|
||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
"declaration": false,
|
"declaration": false,
|
||||||
"moduleResolution": "node",
|
"moduleResolution": "node",
|
||||||
"emitDecoratorMetadata": true,
|
"emitDecoratorMetadata": true,
|
||||||
"experimentalDecorators": true,
|
"experimentalDecorators": true,
|
||||||
|
"target": "es5",
|
||||||
|
"typeRoots": [
|
||||||
|
"node_modules/@types"
|
||||||
|
],
|
||||||
"lib": [
|
"lib": [
|
||||||
"es2016"
|
"es2016",
|
||||||
|
"dom"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -39,7 +39,7 @@ if (!/e2e/.test(location.search)) {
|
|||||||
directives.push(CountdownLocalVarParentComponent);
|
directives.push(CountdownLocalVarParentComponent);
|
||||||
directives.push(CountdownViewChildParentComponent);
|
directives.push(CountdownViewChildParentComponent);
|
||||||
} else {
|
} else {
|
||||||
// In e2e test use CUSTOM_ELEMENTS_SCHEMA to supress unknown element errors
|
// In e2e test use CUSTOM_ELEMENTS_SCHEMA to suppress unknown element errors
|
||||||
schemas.push(CUSTOM_ELEMENTS_SCHEMA);
|
schemas.push(CUSTOM_ELEMENTS_SCHEMA);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
15
aio/content/examples/docs-style-guide/e2e-spec.ts
Normal file
15
aio/content/examples/docs-style-guide/e2e-spec.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
'use strict'; // necessary for es6 output in node
|
||||||
|
|
||||||
|
import { browser, element, by } from 'protractor';
|
||||||
|
|
||||||
|
describe('Docs Style Guide', function () {
|
||||||
|
let _title = 'Authors Style Guide Sample';
|
||||||
|
|
||||||
|
beforeAll(function () {
|
||||||
|
browser.get('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should display correct title: ' + _title, function () {
|
||||||
|
expect(element(by.css('h1')).getText()).toEqual(_title);
|
||||||
|
});
|
||||||
|
});
|
7
aio/content/examples/docs-style-guide/package.1.json
Normal file
7
aio/content/examples/docs-style-guide/package.1.json
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"scripts": {
|
||||||
|
"start": "concurrently \"npm run build:watch\" \"npm run serve\"",
|
||||||
|
"test": "concurrently \"npm run build:watch\" \"karma start karma.conf.js\"",
|
||||||
|
"lint": "tslint ./src/**/*.ts -t verbose"
|
||||||
|
}
|
||||||
|
}
|
10
aio/content/examples/docs-style-guide/plnkr.json
Normal file
10
aio/content/examples/docs-style-guide/plnkr.json
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"description": "Authors style guide",
|
||||||
|
"basePath": "src/",
|
||||||
|
"files": [
|
||||||
|
"!**/*.d.ts",
|
||||||
|
"!**/*.js",
|
||||||
|
"!**/*.[1,2,3].*"
|
||||||
|
],
|
||||||
|
"tags": ["author", "style guide"]
|
||||||
|
}
|
9
aio/content/examples/docs-style-guide/second.plnkr.json
Normal file
9
aio/content/examples/docs-style-guide/second.plnkr.json
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"description": "Second authors style guide plunker (non-executing)",
|
||||||
|
"basePath": "src/",
|
||||||
|
"files": [
|
||||||
|
"index.2.html"
|
||||||
|
],
|
||||||
|
"main": "index.2.html",
|
||||||
|
"tags": ["author", "style guide"]
|
||||||
|
}
|
@ -0,0 +1,51 @@
|
|||||||
|
/* #docregion heroes */
|
||||||
|
.heroes {
|
||||||
|
margin: 0 0 2em 0;
|
||||||
|
list-style-type: none;
|
||||||
|
padding: 0;
|
||||||
|
width: 15em;
|
||||||
|
}
|
||||||
|
/* #enddocregion heroes */
|
||||||
|
|
||||||
|
.heroes li {
|
||||||
|
cursor: pointer;
|
||||||
|
position: relative;
|
||||||
|
left: 0;
|
||||||
|
background-color: #EEE;
|
||||||
|
margin: .5em;
|
||||||
|
padding: .3em 0;
|
||||||
|
height: 1.6em;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
.heroes li.selected:hover {
|
||||||
|
background-color: #BBD8DC !important;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.heroes li:hover {
|
||||||
|
color: #607D8B;
|
||||||
|
background-color: #DDD;
|
||||||
|
left: .1em;
|
||||||
|
}
|
||||||
|
.heroes .text {
|
||||||
|
position: relative;
|
||||||
|
top: -3px;
|
||||||
|
}
|
||||||
|
.heroes .badge {
|
||||||
|
display: inline-block;
|
||||||
|
font-size: small;
|
||||||
|
color: white;
|
||||||
|
padding: 0.8em 0.7em 0 0.7em;
|
||||||
|
background-color: #607D8B;
|
||||||
|
line-height: 1em;
|
||||||
|
position: relative;
|
||||||
|
left: -1px;
|
||||||
|
top: -4px;
|
||||||
|
height: 1.8em;
|
||||||
|
margin-right: .8em;
|
||||||
|
border-radius: 4px 0 0 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected {
|
||||||
|
background-color: #CFD8DC !important;
|
||||||
|
color: white;
|
||||||
|
}
|
@ -0,0 +1,21 @@
|
|||||||
|
<!-- #docplaster -->
|
||||||
|
<!-- #docregion -->
|
||||||
|
<h1>{{title}}</h1>
|
||||||
|
<h2>My Heroes</h2>
|
||||||
|
<ul class="heroes">
|
||||||
|
<li *ngFor="let hero of heroes"
|
||||||
|
[class.selected]="hero === selectedHero"
|
||||||
|
(click)="onSelect(hero)">
|
||||||
|
<span class="badge">{{hero.id}}</span> {{hero.name}}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<div *ngIf="selectedHero">
|
||||||
|
<h2>{{selectedHero.name}} details!</h2>
|
||||||
|
<div><label>id: </label>{{selectedHero.id}}</div>
|
||||||
|
<div>
|
||||||
|
<label>name: </label>
|
||||||
|
<!-- #docregion selected-hero -->
|
||||||
|
<input [(ngModel)]="selectedHero.name" placeholder="name"/>
|
||||||
|
<!-- #enddocregion selected-hero -->
|
||||||
|
</div>
|
||||||
|
</div>
|
@ -0,0 +1,23 @@
|
|||||||
|
// #docplaster
|
||||||
|
// #docregion
|
||||||
|
import { Component } from '@angular/core';
|
||||||
|
import { Hero, HEROES } from './hero';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'my-app',
|
||||||
|
templateUrl: './app.component.html',
|
||||||
|
styleUrls: ['./app.component.css']
|
||||||
|
})
|
||||||
|
// #docregion class, class-skeleton
|
||||||
|
export class AppComponent {
|
||||||
|
// #enddocregion class-skeleton
|
||||||
|
title = 'Authors Style Guide Sample';
|
||||||
|
heroes = HEROES;
|
||||||
|
selectedHero: Hero;
|
||||||
|
|
||||||
|
onSelect(hero: Hero): void {
|
||||||
|
this.selectedHero = hero;
|
||||||
|
}
|
||||||
|
// #docregion class-skeleton
|
||||||
|
}
|
||||||
|
// #enddocregion class, class-skeleton
|
15
aio/content/examples/docs-style-guide/src/app/app.module.ts
Normal file
15
aio/content/examples/docs-style-guide/src/app/app.module.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
// #docregion
|
||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { BrowserModule } from '@angular/platform-browser';
|
||||||
|
import { FormsModule } from '@angular/forms';
|
||||||
|
|
||||||
|
import { AppComponent } from './app.component';
|
||||||
|
|
||||||
|
// #docregion class
|
||||||
|
@NgModule({
|
||||||
|
imports: [ BrowserModule, FormsModule ],
|
||||||
|
declarations: [ AppComponent ],
|
||||||
|
bootstrap: [ AppComponent ]
|
||||||
|
})
|
||||||
|
export class AppModule { }
|
||||||
|
// #enddocregion class
|
11
aio/content/examples/docs-style-guide/src/app/hero.ts
Normal file
11
aio/content/examples/docs-style-guide/src/app/hero.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
export class Hero {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const HEROES: Hero[] = [
|
||||||
|
{ id: 11, name: 'Mr. Nice' },
|
||||||
|
{ id: 12, name: 'Narco' },
|
||||||
|
{ id: 13, name: 'Bombasto' },
|
||||||
|
{ id: 14, name: 'Celeritas' }
|
||||||
|
];
|
10
aio/content/examples/docs-style-guide/src/index.2.html
Normal file
10
aio/content/examples/docs-style-guide/src/index.2.html
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Second Authors Style Guide</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Second Authors Style Guide</h1>
|
||||||
|
<p>Placeholder. Does nothing at all.</p>
|
||||||
|
</body>
|
||||||
|
</html>
|
30
aio/content/examples/docs-style-guide/src/index.html
Normal file
30
aio/content/examples/docs-style-guide/src/index.html
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<!-- #docregion -->
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Docs Style Guide</title>
|
||||||
|
<base href="/">
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
|
||||||
|
<!-- #docregion styles -->
|
||||||
|
<link rel="stylesheet" href="styles.css">
|
||||||
|
<!-- #enddocregion styles -->
|
||||||
|
|
||||||
|
<!-- Polyfills -->
|
||||||
|
<script src="node_modules/core-js/client/shim.min.js"></script>
|
||||||
|
|
||||||
|
<script src="node_modules/zone.js/dist/zone.js"></script>
|
||||||
|
<script src="node_modules/systemjs/dist/system.src.js"></script>
|
||||||
|
|
||||||
|
<script src="systemjs.config.js"></script>
|
||||||
|
<script>
|
||||||
|
System.import('main.js').catch(function(err){ console.error(err); });
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<my-app>Loading...</my-app>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
4
aio/content/examples/docs-style-guide/src/main.ts
Normal file
4
aio/content/examples/docs-style-guide/src/main.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
// #docregion
|
||||||
|
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
|
||||||
|
import { AppModule } from './app/app.module';
|
||||||
|
platformBrowserDynamic().bootstrapModule(AppModule);
|
@ -24,7 +24,7 @@ export class AdBannerComponent implements AfterViewInit, OnDestroy {
|
|||||||
subscription: any;
|
subscription: any;
|
||||||
interval: any;
|
interval: any;
|
||||||
|
|
||||||
constructor(private _componentFactoryResolver: ComponentFactoryResolver) { }
|
constructor(private componentFactoryResolver: ComponentFactoryResolver) { }
|
||||||
|
|
||||||
ngAfterViewInit() {
|
ngAfterViewInit() {
|
||||||
this.loadComponent();
|
this.loadComponent();
|
||||||
@ -39,7 +39,7 @@ export class AdBannerComponent implements AfterViewInit, OnDestroy {
|
|||||||
this.currentAddIndex = (this.currentAddIndex + 1) % this.ads.length;
|
this.currentAddIndex = (this.currentAddIndex + 1) % this.ads.length;
|
||||||
let adItem = this.ads[this.currentAddIndex];
|
let adItem = this.ads[this.currentAddIndex];
|
||||||
|
|
||||||
let componentFactory = this._componentFactoryResolver.resolveComponentFactory(adItem.component);
|
let componentFactory = this.componentFactoryResolver.resolveComponentFactory(adItem.component);
|
||||||
|
|
||||||
let viewContainerRef = this.adHost.viewContainerRef;
|
let viewContainerRef = this.adHost.viewContainerRef;
|
||||||
viewContainerRef.clear();
|
viewContainerRef.clear();
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"description": "Hierachical Dependency Injection",
|
"description": "Hierarchical Dependency Injection",
|
||||||
"basePath": "src/",
|
"basePath": "src/",
|
||||||
"files":[
|
"files":[
|
||||||
"!**/*.d.ts",
|
"!**/*.d.ts",
|
||||||
|
@ -30,4 +30,8 @@ describe('i18n E2E Tests', () => {
|
|||||||
expect(element.all(by.css('span')).get(1).getText()).toBe('El heroe es mujer');
|
expect(element.all(by.css('span')).get(1).getText()).toBe('El heroe es mujer');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should display the nested expression', function() {
|
||||||
|
expect(element.all(by.css('span')).get(2).getText()).toBe('Aquí tenemos: 3 mujeres');
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
@ -2,8 +2,10 @@
|
|||||||
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
|
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
<file source-language="en" datatype="plaintext" original="ng2.template">
|
<file source-language="en" datatype="plaintext" original="ng2.template">
|
||||||
<body>
|
<body>
|
||||||
<trans-unit id="af2ccf4b5dba59616e92cf1531505af02da8f6d2" datatype="html">
|
<trans-unit id="introductionHeader" datatype="html">
|
||||||
<source>Hello i18n!</source>
|
<source>
|
||||||
|
Hello i18n!
|
||||||
|
</source>
|
||||||
<target/>
|
<target/>
|
||||||
<note priority="1" from="description">An introduction header for this sample</note>
|
<note priority="1" from="description">An introduction header for this sample</note>
|
||||||
<note priority="1" from="meaning">User welcome</note>
|
<note priority="1" from="meaning">User welcome</note>
|
||||||
@ -24,12 +26,6 @@ I don't output any element either
|
|||||||
<source>Angular logo</source>
|
<source>Angular logo</source>
|
||||||
<target/>
|
<target/>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="2579611bfcccd75bcd41fac90150d27d6ebb30b8" datatype="html">
|
|
||||||
<source>
|
|
||||||
<x id="START_TAG_SPAN" ctype="x-span"/><x id="ICU"/><x id="CLOSE_TAG_SPAN" ctype="x-span"/>
|
|
||||||
</source>
|
|
||||||
<target/>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="6e22e74e8cbd3095560cfe08993c4fdfa3c50eb0" datatype="html">
|
<trans-unit id="6e22e74e8cbd3095560cfe08993c4fdfa3c50eb0" datatype="html">
|
||||||
<source/>
|
<source/>
|
||||||
<target/>
|
<target/>
|
||||||
@ -42,6 +38,14 @@ I don't output any element either
|
|||||||
<source/>
|
<source/>
|
||||||
<target/>
|
<target/>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
<trans-unit id="2cf9a08c5b6e3612572a2a36dd46563013848382" datatype="html">
|
||||||
|
<source>Here we have: <x id="ICU"/></source>
|
||||||
|
<target/>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="db1b921b55301ce3957e382090729562002da036" datatype="html">
|
||||||
|
<source/>
|
||||||
|
<target/>
|
||||||
|
</trans-unit>
|
||||||
</body>
|
</body>
|
||||||
</file>
|
</file>
|
||||||
</xliff>
|
</xliff>
|
||||||
|
@ -10,6 +10,23 @@
|
|||||||
<h1 i18n="An introduction header for this sample">Hello i18n!</h1>
|
<h1 i18n="An introduction header for this sample">Hello i18n!</h1>
|
||||||
<!--#enddocregion i18n-attribute-desc-->
|
<!--#enddocregion i18n-attribute-desc-->
|
||||||
|
|
||||||
|
<!--#docregion i18n-attribute-meaning-->
|
||||||
|
<h1 i18n="site header|An introduction header for this sample">Hello i18n!</h1>
|
||||||
|
<!--#enddocregion i18n-attribute-meaning-->
|
||||||
|
|
||||||
|
<!--#docregion i18n-attribute-id-->
|
||||||
|
<h1 i18n="An introduction header for this sample@@introductionHeader">Hello i18n!</h1>
|
||||||
|
<!--#enddocregion i18n-attribute-id-->
|
||||||
|
|
||||||
|
<!--#docregion i18n-attribute-meaning-and-id-->
|
||||||
|
<h1 i18n="site header|An introduction header for this sample@@introductionHeader">Hello i18n!</h1>
|
||||||
|
<!--#enddocregion i18n-attribute-meaning-and-id-->
|
||||||
|
|
||||||
|
<!--#docregion i18n-attribute-solo-id-->
|
||||||
|
<h1 i18n="@@introductionHeader">Hello i18n!</h1>
|
||||||
|
<!--#enddocregion i18n-attribute-solo-id-->
|
||||||
|
|
||||||
<!--#docregion i18n-title-->
|
<!--#docregion i18n-title-->
|
||||||
<img [src]="logo" title="Angular logo">
|
<img [src]="logo" title="Angular logo">
|
||||||
<!--#enddocregion i18n-title-->
|
<!--#enddocregion i18n-title-->
|
||||||
|
Contact GitHub API Training Shop Blog About
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
<!--#docregion-->
|
<!--#docregion-->
|
||||||
<!--#docregion i18n-attribute-meaning-->
|
<!--#docregion i18n-attribute-meaning-->
|
||||||
<h1 i18n="User welcome|An introduction header for this sample">Hello i18n!</h1>
|
<h1 i18n="User welcome|An introduction header for this sample@@introductionHeader">
|
||||||
|
Hello i18n!
|
||||||
|
</h1>
|
||||||
<!--#enddocregion i18n-attribute-meaning-->
|
<!--#enddocregion i18n-attribute-meaning-->
|
||||||
|
|
||||||
<!--#docregion i18n-ng-container-->
|
<!--#docregion i18n-ng-container-->
|
||||||
@ -31,4 +33,11 @@ I don't output any element either
|
|||||||
<!--#docregion i18n-select-->
|
<!--#docregion i18n-select-->
|
||||||
<span i18n>The hero is {gender, select, m {male} f {female}}</span>
|
<span i18n>The hero is {gender, select, m {male} f {female}}</span>
|
||||||
<!--#enddocregion i18n-select-->
|
<!--#enddocregion i18n-select-->
|
||||||
<br>
|
<br><br>
|
||||||
|
<!--#docregion i18n-nested-->
|
||||||
|
<span i18n>Here we have: {count, plural,
|
||||||
|
=0 {no one}
|
||||||
|
=1 {one {gender, select, male {man} female {woman}}}
|
||||||
|
other {{{heroes.length}} {gender, select, male {men} female {women}}}
|
||||||
|
}</span>
|
||||||
|
<!--#enddocregion i18n-nested-->
|
||||||
|
@ -10,6 +10,8 @@ export class AppComponent {
|
|||||||
gender = 'f';
|
gender = 'f';
|
||||||
fly = true;
|
fly = true;
|
||||||
logo = 'https://angular.io/resources/images/logos/angular/angular.png';
|
logo = 'https://angular.io/resources/images/logos/angular/angular.png';
|
||||||
|
count = 3;
|
||||||
|
heroes: string[] = ['Magneta', 'Celeritas', 'Dynama'];
|
||||||
inc(i: number) {
|
inc(i: number) {
|
||||||
this.wolves = Math.min(5, Math.max(0, this.wolves + i));
|
this.wolves = Math.min(5, Math.max(0, this.wolves + i));
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
// #docregion
|
// #docplaster
|
||||||
import { TRANSLATIONS, TRANSLATIONS_FORMAT, LOCALE_ID } from '@angular/core';
|
// #docregion without-missing-translation
|
||||||
|
import { TRANSLATIONS, TRANSLATIONS_FORMAT, LOCALE_ID, MissingTranslationStrategy } from '@angular/core';
|
||||||
|
import { CompilerConfig } from '@angular/compiler';
|
||||||
|
|
||||||
export function getTranslationProviders(): Promise<Object[]> {
|
export function getTranslationProviders(): Promise<Object[]> {
|
||||||
|
|
||||||
@ -17,13 +19,18 @@ export function getTranslationProviders(): Promise<Object[]> {
|
|||||||
// Ex: 'locale/messages.es.xlf`
|
// Ex: 'locale/messages.es.xlf`
|
||||||
const translationFile = `./locale/messages.${locale}.xlf`;
|
const translationFile = `./locale/messages.${locale}.xlf`;
|
||||||
|
|
||||||
|
// #docregion missing-translation
|
||||||
return getTranslationsWithSystemJs(translationFile)
|
return getTranslationsWithSystemJs(translationFile)
|
||||||
.then( (translations: string ) => [
|
.then( (translations: string ) => [
|
||||||
{ provide: TRANSLATIONS, useValue: translations },
|
{ provide: TRANSLATIONS, useValue: translations },
|
||||||
{ provide: TRANSLATIONS_FORMAT, useValue: 'xlf' },
|
{ provide: TRANSLATIONS_FORMAT, useValue: 'xlf' },
|
||||||
{ provide: LOCALE_ID, useValue: locale }
|
{ provide: LOCALE_ID, useValue: locale },
|
||||||
|
// #enddocregion without-missing-translation
|
||||||
|
{ provide: CompilerConfig, useValue: new CompilerConfig({ missingTranslation: MissingTranslationStrategy.Error }) }
|
||||||
|
// #docregion without-missing-translation
|
||||||
])
|
])
|
||||||
.catch(() => noProviders); // ignore if file not found
|
.catch(() => noProviders); // ignore if file not found
|
||||||
|
// #enddocregion missing-translation
|
||||||
}
|
}
|
||||||
|
|
||||||
declare var System: any;
|
declare var System: any;
|
||||||
@ -31,3 +38,4 @@ declare var System: any;
|
|||||||
function getTranslationsWithSystemJs(file: string) {
|
function getTranslationsWithSystemJs(file: string) {
|
||||||
return System.import(file + '!text'); // relies on text plugin
|
return System.import(file + '!text'); // relies on text plugin
|
||||||
}
|
}
|
||||||
|
// #enddocregion without-missing-translation
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
|
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
<file source-language="en" datatype="plaintext" original="ng2.template">
|
<file source-language="en" datatype="plaintext" original="ng2.template">
|
||||||
<body>
|
<body>
|
||||||
<trans-unit id="af2ccf4b5dba59616e92cf1531505af02da8f6d2" datatype="html">
|
<trans-unit id="introductionHeader" datatype="html">
|
||||||
<source>Hello i18n!</source>
|
<source>Hello i18n!</source>
|
||||||
<target>¡Hola i18n!</target>
|
<target>¡Hola i18n!</target>
|
||||||
<note priority="1" from="description">An introduction header for this sample</note>
|
<note priority="1" from="description">An introduction header for this sample</note>
|
||||||
@ -36,6 +36,20 @@ I don't output any element either
|
|||||||
<source/>
|
<source/>
|
||||||
<target>{gender, select, m {hombre} f {mujer}}</target>
|
<target>{gender, select, m {hombre} f {mujer}}</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
<trans-unit id="2cf9a08c5b6e3612572a2a36dd46563013848382" datatype="html">
|
||||||
|
<source>Here we have: <x id="ICU"/></source>
|
||||||
|
<target>Aquí tenemos: <x id="ICU"/></target>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="db1b921b55301ce3957e382090729562002da036" datatype="html">
|
||||||
|
<source/>
|
||||||
|
<target>
|
||||||
|
{count, plural,
|
||||||
|
=0 { nadie }
|
||||||
|
=1 {{gender, select, m {un hombre} f {una mujer}}}
|
||||||
|
other {{{heroes.length}} {gender, select, m {hombres} f {mujeres}}}
|
||||||
|
}
|
||||||
|
</target>
|
||||||
|
</trans-unit>
|
||||||
</body>
|
</body>
|
||||||
</file>
|
</file>
|
||||||
</xliff>
|
</xliff>
|
||||||
|
@ -5,7 +5,9 @@
|
|||||||
<file source-language="en" datatype="plaintext" original="ng2.template">
|
<file source-language="en" datatype="plaintext" original="ng2.template">
|
||||||
<body>
|
<body>
|
||||||
<!-- #docregion translated-hello -->
|
<!-- #docregion translated-hello -->
|
||||||
<trans-unit id="af2ccf4b5dba59616e92cf1531505af02da8f6d2" datatype="html">
|
<!-- #docregion custom-id -->
|
||||||
|
<trans-unit id="introductionHeader" datatype="html">
|
||||||
|
<!-- #enddocregion custom-id -->
|
||||||
<source>Hello i18n!</source>
|
<source>Hello i18n!</source>
|
||||||
<target>¡Hola i18n!</target>
|
<target>¡Hola i18n!</target>
|
||||||
<note priority="1" from="description">An introduction header for this sample</note>
|
<note priority="1" from="description">An introduction header for this sample</note>
|
||||||
@ -13,7 +15,9 @@
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<!-- #enddocregion translated-hello -->
|
<!-- #enddocregion translated-hello -->
|
||||||
<!-- #docregion translated-other-nodes -->
|
<!-- #docregion translated-other-nodes -->
|
||||||
|
<!-- #docregion generated-id -->
|
||||||
<trans-unit id="ba0cc104d3d69bf669f97b8d96a4c5d8d9559aa3" datatype="html">
|
<trans-unit id="ba0cc104d3d69bf669f97b8d96a4c5d8d9559aa3" datatype="html">
|
||||||
|
<!-- #enddocregion generated-id -->
|
||||||
<source>I don't output any element</source>
|
<source>I don't output any element</source>
|
||||||
<target>No genero ningún elemento</target>
|
<target>No genero ningún elemento</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
@ -48,6 +52,34 @@
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<!-- #enddocregion translate-select-2 -->
|
<!-- #enddocregion translate-select-2 -->
|
||||||
<!-- #enddocregion translated-select -->
|
<!-- #enddocregion translated-select -->
|
||||||
|
<trans-unit id="db04527df562d12c8607eab2b5723ef6e2066ba0" datatype="html">
|
||||||
|
<source>Here we have: <x id="ICU"/></source>
|
||||||
|
<target/>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="000058be4e6f08b685d1d0a70f9da68067df7379" datatype="html">
|
||||||
|
<source/>
|
||||||
|
<target/>
|
||||||
|
</trans-unit>
|
||||||
|
<!-- #docregion translate-nested -->
|
||||||
|
<!-- #docregion translate-nested-1 -->
|
||||||
|
<trans-unit id="2cf9a08c5b6e3612572a2a36dd46563013848382" datatype="html">
|
||||||
|
<source>Here we have: <x id="ICU"/></source>
|
||||||
|
<target>Aquí tenemos: <x id="ICU"/></target>
|
||||||
|
</trans-unit>
|
||||||
|
<!-- #enddocregion translate-nested-1 -->
|
||||||
|
<!-- #docregion translate-nested-2 -->
|
||||||
|
<trans-unit id="db1b921b55301ce3957e382090729562002da036" datatype="html">
|
||||||
|
<source/>
|
||||||
|
<target>
|
||||||
|
{count, plural,
|
||||||
|
=0 { nadie }
|
||||||
|
=1 {{gender, select, m {un hombre} f {una mujer}}}
|
||||||
|
other {{{heroes.length}} {gender, select, m {hombres} f {mujeres}}}
|
||||||
|
}
|
||||||
|
</target>
|
||||||
|
</trans-unit>
|
||||||
|
<!-- #enddocregion translate-nested-2 -->
|
||||||
|
<!-- #enddocregion translate-nested -->
|
||||||
</body>
|
</body>
|
||||||
</file>
|
</file>
|
||||||
</xliff>
|
</xliff>
|
||||||
|
@ -14,6 +14,6 @@ export class CrisisDetailComponent implements OnInit {
|
|||||||
constructor(private route: ActivatedRoute) { }
|
constructor(private route: ActivatedRoute) { }
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
this.id = parseInt(this.route.snapshot.params['id'], 10);
|
this.id = parseInt(this.route.snapshot.paramMap.get('id'), 10);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -25,7 +25,7 @@ export class HeroDetailComponent implements OnInit {
|
|||||||
private heroService: HeroService) { }
|
private heroService: HeroService) { }
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
let id = parseInt(this.route.snapshot.params['id'], 10);
|
let id = parseInt(this.route.snapshot.paramMap.get('id'), 10);
|
||||||
this.heroService.getHero(id).then(hero => this.hero = hero);
|
this.heroService.getHero(id).then(hero => this.hero = hero);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -85,7 +85,7 @@ describe('Pipes', function () {
|
|||||||
return resetEle.click();
|
return resetEle.click();
|
||||||
})
|
})
|
||||||
.then(function() {
|
.then(function() {
|
||||||
expect(flyingHeroesEle.count()).toEqual(2, 'reset should restore orginal flying heroes');
|
expect(flyingHeroesEle.count()).toEqual(2, 'reset should restore original flying heroes');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -22,8 +22,8 @@ export class AdminDashboardComponent implements OnInit {
|
|||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
// Capture the session ID if available
|
// Capture the session ID if available
|
||||||
this.sessionId = this.route
|
this.sessionId = this.route
|
||||||
.queryParams
|
.queryParamMap
|
||||||
.map(params => params['session_id'] || 'None');
|
.map(params => params.get('session_id') || 'None');
|
||||||
|
|
||||||
// Capture the fragment if available
|
// Capture the fragment if available
|
||||||
this.token = this.route
|
this.token = this.route
|
||||||
|
@ -36,8 +36,8 @@ export class AdminDashboardComponent implements OnInit {
|
|||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
// Capture the session ID if available
|
// Capture the session ID if available
|
||||||
this.sessionId = this.route
|
this.sessionId = this.route
|
||||||
.queryParams
|
.queryParamMap
|
||||||
.map(params => params['session_id'] || 'None');
|
.map(params => params.get('session_id') || 'None');
|
||||||
|
|
||||||
// Capture the fragment if available
|
// Capture the fragment if available
|
||||||
this.token = this.route
|
this.token = this.route
|
||||||
|
@ -17,7 +17,10 @@ const appRoutes: Routes = [
|
|||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
RouterModule.forRoot(appRoutes)
|
RouterModule.forRoot(
|
||||||
|
appRoutes,
|
||||||
|
{ enableTracing: true } // <-- debugging purposes only
|
||||||
|
)
|
||||||
],
|
],
|
||||||
exports: [
|
exports: [
|
||||||
RouterModule
|
RouterModule
|
||||||
|
@ -15,7 +15,10 @@ const appRoutes: Routes = [
|
|||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
RouterModule.forRoot(appRoutes)
|
RouterModule.forRoot(
|
||||||
|
appRoutes,
|
||||||
|
{ enableTracing: true } // <-- debugging purposes only
|
||||||
|
)
|
||||||
],
|
],
|
||||||
exports: [
|
exports: [
|
||||||
RouterModule
|
RouterModule
|
||||||
|
@ -22,7 +22,10 @@ const appRoutes: Routes = [
|
|||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
RouterModule.forRoot(appRoutes)
|
RouterModule.forRoot(
|
||||||
|
appRoutes,
|
||||||
|
{ enableTracing: true } // <-- debugging purposes only
|
||||||
|
)
|
||||||
],
|
],
|
||||||
exports: [
|
exports: [
|
||||||
RouterModule
|
RouterModule
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user