Compare commits
2 Commits
4.0.0
...
2.0.0-beta
Author | SHA1 | Date | |
---|---|---|---|
4f1f29d7d2 | |||
b1b73dc39e |
6
.gitattributes
vendored
6
.gitattributes
vendored
@ -1,9 +1,5 @@
|
||||
# Auto detect text files and perform LF normalization
|
||||
* text=auto
|
||||
|
||||
# JS and TS files must always use LF for tools to work
|
||||
# JS files must always use LF for tools to work
|
||||
*.js eol=lf
|
||||
*.ts eol=lf
|
||||
|
||||
# Must keep Windows line ending to be parsed correctly
|
||||
scripts/windows/packages.txt eol=crlf
|
||||
|
39
.github/ISSUE_TEMPLATE.md
vendored
39
.github/ISSUE_TEMPLATE.md
vendored
@ -1,39 +0,0 @@
|
||||
<!--
|
||||
IF YOU DON'T FILL OUT THE FOLLOWING INFORMATION WE MIGHT CLOSE YOUR ISSUE WITHOUT INVESTIGATING
|
||||
-->
|
||||
|
||||
**I'm submitting a ...** (check one with "x")
|
||||
```
|
||||
[ ] bug report => search github for a similar issue or PR before submitting
|
||||
[ ] feature request
|
||||
[ ] support request => Please do not submit support request here, instead see https://github.com/angular/angular/blob/master/CONTRIBUTING.md#question
|
||||
```
|
||||
|
||||
**Current behavior**
|
||||
<!-- Describe how the bug manifests. -->
|
||||
|
||||
**Expected behavior**
|
||||
<!-- Describe what the behavior would be without the bug. -->
|
||||
|
||||
**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,
|
||||
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).
|
||||
-->
|
||||
|
||||
**What is the motivation / use case for changing the behavior?**
|
||||
<!-- 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
|
||||
<!-- 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 ]
|
||||
<!-- All browsers where this could be reproduced -->
|
||||
|
||||
* **Language:** [all | TypeScript X.X | ES6/7 | ES5]
|
||||
|
||||
* **Node (for AoT issues):** `node --version` =
|
36
.github/PULL_REQUEST_TEMPLATE.md
vendored
36
.github/PULL_REQUEST_TEMPLATE.md
vendored
@ -1,36 +0,0 @@
|
||||
**Please check if the PR fulfills these requirements**
|
||||
- [ ] 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)
|
||||
- [ ] Docs have been added / updated (for bug fixes / features)
|
||||
|
||||
|
||||
**What kind of change does this PR introduce?** (check one with "x")
|
||||
```
|
||||
[ ] Bugfix
|
||||
[ ] Feature
|
||||
[ ] Code style update (formatting, local variables)
|
||||
[ ] Refactoring (no functional changes, no api changes)
|
||||
[ ] Build related changes
|
||||
[ ] CI related changes
|
||||
[ ] Other... Please describe:
|
||||
```
|
||||
|
||||
**What is the current behavior?** (You can also link to an open issue here)
|
||||
|
||||
|
||||
|
||||
**What is the new behavior?**
|
||||
|
||||
|
||||
|
||||
**Does this PR introduce a breaking change?** (check one with "x")
|
||||
```
|
||||
[ ] Yes
|
||||
[ ] No
|
||||
```
|
||||
|
||||
If this PR contains a breaking change, please describe the impact and migration path for existing applications: ...
|
||||
|
||||
|
||||
**Other information**:
|
||||
|
30
.gitignore
vendored
30
.gitignore
vendored
@ -1,9 +1,30 @@
|
||||
.DS_STORE
|
||||
|
||||
# Don’t commit the following directories created by pub.
|
||||
packages
|
||||
pubspec.lock
|
||||
.pub
|
||||
.packages
|
||||
|
||||
/dist/
|
||||
.buildlog
|
||||
node_modules
|
||||
bower_components
|
||||
|
||||
# Or broccoli working directory
|
||||
tmp
|
||||
|
||||
# Or the files created by dart2js.
|
||||
*.dart.js
|
||||
*.dart.precompiled.js
|
||||
*.js_
|
||||
*.js.deps
|
||||
*.js.map
|
||||
|
||||
# Or type definitions we mirror from github
|
||||
**/typings/**/*.d.ts
|
||||
**/typings/tsd.cached.json
|
||||
|
||||
# Include when developing application packages.
|
||||
pubspec.lock
|
||||
.c9
|
||||
@ -17,12 +38,13 @@ modules/.vscode
|
||||
# Don't check in secret files
|
||||
*secret.js
|
||||
|
||||
# Ignore npm/yarn debug log
|
||||
# Ignore npm debug log
|
||||
npm-debug.log
|
||||
yarn-error.log
|
||||
|
||||
/docs/bower_components/
|
||||
|
||||
# build-analytics
|
||||
.build-analytics
|
||||
|
||||
# rollup-test output
|
||||
/modules/rollup-test/dist/
|
||||
# built dart payload tests
|
||||
/modules_dart/payload/**/build
|
||||
|
256
.pullapprove.yml
256
.pullapprove.yml
@ -1,256 +0,0 @@
|
||||
# Configuration for pullapprove.com
|
||||
#
|
||||
# Approval access and primary role is determined by info in the project ownership spreadsheet:
|
||||
# https://docs.google.com/spreadsheets/d/1-HIlzfbPYGsPr9KuYMe6bLfc4LXzPjpoALqtYRYTZB0/edit?pli=1#gid=0&vpid=A5
|
||||
#
|
||||
# === GitHub username to Full name map ===
|
||||
#
|
||||
# alexeagle - Alex Eagle
|
||||
# alxhub - Alex Rickabaugh
|
||||
# chuckjaz - Chuck Jazdzewski
|
||||
# gkalpak - George Kalpakas
|
||||
# IgorMinar - Igor Minar
|
||||
# jasonaden - Jason Aden
|
||||
# kara - Kara Erickson
|
||||
# matsko - Matias Niemelä
|
||||
# mhevery - Misko Hevery
|
||||
# petebacondarwin - Pete Bacon Darwin
|
||||
# pkozlowski-opensource - Pawel Kozlowski
|
||||
# robwormald - Rob Wormald
|
||||
# tbosch - Tobias Bosch
|
||||
# vicb - Victor Berchet
|
||||
# vikerman - Vikram Subramanian
|
||||
# wardbell - Ward Bell
|
||||
# tinayuangao - Tina Gao
|
||||
|
||||
version: 2
|
||||
|
||||
group_defaults:
|
||||
required: 1
|
||||
reset_on_reopened:
|
||||
enabled: true
|
||||
approve_by_comment:
|
||||
enabled: false
|
||||
|
||||
groups:
|
||||
root:
|
||||
conditions:
|
||||
files:
|
||||
include:
|
||||
- "*"
|
||||
exclude:
|
||||
- "aio/*"
|
||||
- "integration/*"
|
||||
- "modules/*"
|
||||
- "packages/*"
|
||||
- "tools/*"
|
||||
users:
|
||||
- IgorMinar
|
||||
- mhevery
|
||||
|
||||
public-api:
|
||||
conditions:
|
||||
files:
|
||||
include:
|
||||
- "tools/public_api_guard/*"
|
||||
users:
|
||||
- IgorMinar
|
||||
- mhevery
|
||||
|
||||
build-and-ci:
|
||||
conditions:
|
||||
files:
|
||||
include:
|
||||
- "*.yml"
|
||||
- "*.json"
|
||||
- "*.lock"
|
||||
- "tools/*"
|
||||
exclude:
|
||||
- "tools/@angular/tsc-wrapped/*"
|
||||
- "tools/public_api_guard/*"
|
||||
- "aio/*"
|
||||
users:
|
||||
- IgorMinar #primary
|
||||
- alexeagle
|
||||
- jasonaden
|
||||
- mhevery #fallback
|
||||
|
||||
integration:
|
||||
conditions:
|
||||
files:
|
||||
- "integration/*"
|
||||
users:
|
||||
- alexeagle
|
||||
- mhevery
|
||||
- tbosch
|
||||
- vicb
|
||||
- IgorMinar #fallback
|
||||
|
||||
|
||||
core:
|
||||
conditions:
|
||||
files:
|
||||
- "packages/core/*"
|
||||
users:
|
||||
- tbosch #primary
|
||||
- mhevery
|
||||
- vicb
|
||||
- IgorMinar #fallback
|
||||
|
||||
compiler/animations:
|
||||
conditions:
|
||||
files:
|
||||
- "packages/compiler/src/animation/*"
|
||||
users:
|
||||
- matsko #primary
|
||||
- tbosch
|
||||
- IgorMinar #fallback
|
||||
- mhevery #fallback
|
||||
|
||||
compiler/i18n:
|
||||
conditions:
|
||||
files:
|
||||
- "packages/compiler/src/i18n/*"
|
||||
users:
|
||||
- vicb #primary
|
||||
- tbosch
|
||||
- IgorMinar #fallback
|
||||
- mhevery #fallback
|
||||
|
||||
compiler:
|
||||
conditions:
|
||||
files:
|
||||
- "packages/compiler/*"
|
||||
users:
|
||||
- tbosch #primary
|
||||
- vicb
|
||||
- chuckjaz
|
||||
- mhevery
|
||||
- IgorMinar #fallback
|
||||
|
||||
compiler-cli:
|
||||
conditions:
|
||||
files:
|
||||
- "tools/@angular/tsc-wrapped/*"
|
||||
- "packages/compiler-cli/*"
|
||||
users:
|
||||
- alexeagle
|
||||
- chuckjaz
|
||||
- tbosch
|
||||
- IgorMinar #fallback
|
||||
- mhevery #fallback
|
||||
|
||||
common:
|
||||
conditions:
|
||||
files:
|
||||
- "packages/common/*"
|
||||
users:
|
||||
- pkozlowski-opensource #primary
|
||||
- vicb
|
||||
- IgorMinar #fallback
|
||||
- mhevery #fallback
|
||||
|
||||
forms:
|
||||
conditions:
|
||||
files:
|
||||
- "packages/forms/*"
|
||||
users:
|
||||
- kara #primary
|
||||
- tinayuangao #secondary
|
||||
- IgorMinar #fallback
|
||||
- mhevery #fallback
|
||||
|
||||
http:
|
||||
conditions:
|
||||
files:
|
||||
- "packages/http/*"
|
||||
users:
|
||||
- vikerman #primary
|
||||
- alxhub
|
||||
- IgorMinar #fallback
|
||||
- mhevery #fallback
|
||||
|
||||
language-service:
|
||||
conditions:
|
||||
files:
|
||||
- "packages/language-service/*"
|
||||
users:
|
||||
- chuckjaz #primary
|
||||
# needs secondary
|
||||
- IgorMinar #fallback
|
||||
- mhevery #fallback
|
||||
|
||||
router:
|
||||
conditions:
|
||||
files:
|
||||
- "packages/router/*"
|
||||
users:
|
||||
- vicb #primary
|
||||
# needs secondary
|
||||
- IgorMinar #fallback
|
||||
- mhevery #fallback
|
||||
|
||||
upgrade:
|
||||
conditions:
|
||||
files:
|
||||
- "packages/upgrade/*"
|
||||
users:
|
||||
- petebacondarwin #primary
|
||||
- gkalpak
|
||||
- IgorMinar #fallback
|
||||
- mhevery #fallback
|
||||
|
||||
platform-browser:
|
||||
conditions:
|
||||
files:
|
||||
- "packages/platform-browser/*"
|
||||
users:
|
||||
- tbosch #primary
|
||||
- vicb #secondary
|
||||
- IgorMinar #fallback
|
||||
- mhevery #fallback
|
||||
|
||||
platform-server:
|
||||
conditions:
|
||||
files:
|
||||
- "packages/platform-server/*"
|
||||
users:
|
||||
- vikerman #primary
|
||||
- alxhub
|
||||
- vicb
|
||||
- tbosch
|
||||
- IgorMinar #fallback
|
||||
- mhevery #fallback
|
||||
|
||||
platform-webworker:
|
||||
conditions:
|
||||
files:
|
||||
- "packages/platform-webworker/*"
|
||||
users:
|
||||
- vicb #primary
|
||||
- tbosch #secondary
|
||||
- IgorMinar #fallback
|
||||
- mhevery #fallback
|
||||
|
||||
|
||||
|
||||
benchpress:
|
||||
conditions:
|
||||
files:
|
||||
- "packages/benchpress/*"
|
||||
users:
|
||||
- tbosch #primary
|
||||
# needs secondary
|
||||
- IgorMinar #fallback
|
||||
- mhevery #fallback
|
||||
|
||||
angular.io:
|
||||
conditions:
|
||||
files:
|
||||
- "aio/*"
|
||||
users:
|
||||
- IgorMinar #primary
|
||||
- petebacondarwin #secondary
|
||||
- gkalpak
|
||||
- wardbell
|
||||
- mhevery #fallback
|
143
.travis.yml
143
.travis.yml
@ -1,73 +1,116 @@
|
||||
language: node_js
|
||||
sudo: false
|
||||
node_js:
|
||||
- '6.9.5'
|
||||
- '5.4.1'
|
||||
|
||||
addons:
|
||||
# firefox: "38.0"
|
||||
apt:
|
||||
sources:
|
||||
# needed to install g++ that is used by npms's native modules
|
||||
- ubuntu-toolchain-r-test
|
||||
packages:
|
||||
# needed to install g++ that is used by npms's native modules
|
||||
- g++-4.8
|
||||
# https://docs.travis-ci.com/user/jwt
|
||||
jwt:
|
||||
# SAUCE_ACCESS_KEY<=secret for NGBUILDS_IO_KEY to work around travis-ci/travis-ci#7223, unencrypted value in valentine as NGBUILDS_IO_KEY>
|
||||
# we alias NGBUILDS_IO_KEY to $SAUCE_ACCESS_KEY in env.sh and set the SAUCE_ACCESS_KEY there
|
||||
- secure: "L7nrZwkAtFtYrP2DykPXgZvEKjkv0J/TwQ/r2QGxFTaBq4VZn+2Dw0YS7uCxoMqYzDwH0aAOqxoutibVpk8Z/16nE3tNmU5RzltMd6Xmt3qU2f/JDQLMo6PSlBodnjOUsDHJgmtrcbjhqrx/znA237BkNUu6UZRT7mxhXIZpn0U="
|
||||
branches:
|
||||
except:
|
||||
- g3
|
||||
- g3_v2_0
|
||||
|
||||
cache:
|
||||
yarn: true
|
||||
directories:
|
||||
- ./node_modules
|
||||
- ./.chrome/chromium
|
||||
- ./aio/node_modules
|
||||
- node_modules
|
||||
- $HOME/.pub-cache
|
||||
|
||||
env:
|
||||
global:
|
||||
# GITHUB_TOKEN_ANGULAR=<github token, a personal access token of the angular-builds account, account access in valentine>
|
||||
# This is needed for the e2e Travis matrix task to publish packages to github for continuous packages delivery.
|
||||
- secure: "rNqXoy2gqjbF5tBXlRBy+oiYntO3BtzcxZuEtlLMzNaTNzC4dyMOFub0GkzIPWwOzkARoEU9Kv+bC97fDVbCBUKeyzzEqxqddUKhzRxeaYjsefJ6XeTvBvDxwo7wDwyxZSuWdBeGAe4eARVHm7ypsd+AlvqxtzjyS27TK2BzdL4="
|
||||
# FIREBASE_TOKEN
|
||||
# This is needed for publishing builds to the "aio-staging" firebase site.
|
||||
# TODO(i): the token was generated using the iminar@google account, we should switch to a shared/role-base account.
|
||||
- secure: "MPx3UM77o5IlhT75PKHL0FXoB5tSXDc3vnCXCd1sRy4XUTZ9vjcV6nNuyqEf+SOw659bGbC1FI4mACGx1Q+z7MQDR85b1mcA9uSgHDkh+IR82CnCVdaX9d1RXafdJIArahxfmorbiiPPLyPIKggo7ituRm+2c+iraoCkE/pXxYg="
|
||||
- KARMA_BROWSERS=DartiumWithWebPlatform
|
||||
- E2E_BROWSERS=Dartium
|
||||
- LOGS_DIR=/tmp/angular-build/logs
|
||||
- SAUCE_USERNAME=angular-ci
|
||||
- SAUCE_ACCESS_KEY=9b988f434ff8-fbca-8aa4-4ae3-35442987
|
||||
- BROWSER_STACK_USERNAME=angularteam1
|
||||
- BROWSER_STACK_ACCESS_KEY=BWCd4SynLzdDcv8xtzsB
|
||||
- ARCH=linux-x64
|
||||
- DART_DEV_VERSION=latest
|
||||
- DART_STABLE_VERSION=latest
|
||||
# Token for tsd to increase github rate limit
|
||||
# See https://github.com/DefinitelyTyped/tsd#tsdrc
|
||||
# This does not use http://docs.travis-ci.com/user/environment-variables/#Secure-Variables
|
||||
# because those are not visible for pull requests, and those should also be reliable.
|
||||
# This SSO token belongs to github account angular-github-ratelimit-token which has no access
|
||||
# (password is in Valentine)
|
||||
- TSDRC='{"token":"ef474500309daea53d5991b3079159a29520a40b"}'
|
||||
# GITHUB_TOKEN_ANGULAR
|
||||
- secure: "fq/U7VDMWO8O8SnAQkdbkoSe2X92PVqg4d044HmRYVmcf6YbO48+xeGJ8yOk0pCBwl3ISO4Q2ot0x546kxfiYBuHkZetlngZxZCtQiFT9kyId8ZKcYdXaIW9OVdw3Gh3tQyUwDucfkVhqcs52D6NZjyE2aWZ4/d1V4kWRO/LMgo="
|
||||
matrix:
|
||||
# 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_2
|
||||
- CI_MODE=js
|
||||
- CI_MODE=saucelabs_required
|
||||
- CI_MODE=browserstack_required
|
||||
- CI_MODE=saucelabs_optional
|
||||
- CI_MODE=browserstack_optional
|
||||
- CI_MODE=docs_test
|
||||
- CI_MODE=aio
|
||||
- MODE=dart DART_CHANNEL=stable DART_VERSION=$DART_STABLE_VERSION
|
||||
# Disable dart dev build, which is timing out after 2h. #6823
|
||||
# - MODE=dart DART_CHANNEL=dev DART_VERSION=$DART_DEV_VERSION
|
||||
- MODE=saucelabs_required DART_CHANNEL=dev DART_VERSION=$DART_DEV_VERSION
|
||||
- MODE=browserstack_required DART_CHANNEL=dev DART_VERSION=$DART_DEV_VERSION
|
||||
- MODE=saucelabs_optional DART_CHANNEL=dev DART_VERSION=$DART_DEV_VERSION
|
||||
- MODE=browserstack_optional DART_CHANNEL=dev DART_VERSION=$DART_DEV_VERSION
|
||||
- MODE=dart_ddc DART_CHANNEL=dev DART_VERSION=$DART_DEV_VERSION
|
||||
- MODE=js DART_CHANNEL=dev DART_VERSION=$DART_DEV_VERSION
|
||||
- MODE=router DART_CHANNEL=dev DART_VERSION=$DART_DEV_VERSION
|
||||
- MODE=build_only DART_CHANNEL=stable DART_VERSION=$DART_STABLE_VERSION
|
||||
- MODE=lint DART_CHANNEL=dev DART_VERSION=$DART_DEV_VERSION
|
||||
- MODE=payload DART_CHANNEL=stable DART_VERSION=$DART_STABLE_VERSION
|
||||
|
||||
matrix:
|
||||
fast_finish: true
|
||||
allow_failures:
|
||||
- env: "CI_MODE=saucelabs_optional"
|
||||
- env: "CI_MODE=browserstack_optional"
|
||||
- env: "MODE=saucelabs_optional DART_CHANNEL=dev DART_VERSION=$DART_DEV_VERSION"
|
||||
- env: "MODE=browserstack_optional DART_CHANNEL=dev DART_VERSION=$DART_DEV_VERSION"
|
||||
# TODO(alxhub): remove when dartdoc #1039 is in dev channel
|
||||
- env: "MODE=dart DART_CHANNEL=dev DART_VERSION=$DART_DEV_VERSION"
|
||||
|
||||
addons:
|
||||
firefox: "38.0"
|
||||
|
||||
before_install:
|
||||
# source the env.sh script so that the exported variables are available to other scripts later on
|
||||
- source ./scripts/ci/env.sh print
|
||||
- npm install -g npm@3.5.3
|
||||
- node tools/analytics/build-analytics start ci job
|
||||
- node tools/analytics/build-analytics start ci before_install
|
||||
- echo ${TSDRC} > .tsdrc
|
||||
- export DISPLAY=:99.0
|
||||
- export GIT_SHA=$(git rev-parse HEAD)
|
||||
- ./scripts/ci/init_android.sh
|
||||
- ./scripts/ci/install_dart.sh ${DART_CHANNEL} ${DART_VERSION} ${ARCH}
|
||||
- sh -e /etc/init.d/xvfb start
|
||||
- if [[ -e SKIP_TRAVIS_TESTS ]]; then { cat SKIP_TRAVIS_TESTS ; exit 0; } fi
|
||||
- '[ "${TRAVIS_PULL_REQUEST}" = "false" ] && [ "${TRAVIS_BRANCH}" = "master" ] && SAUCE_USERNAME="angular2-ci" && SAUCE_ACCESS_KEY="693ebc16208a-0b5b-1614-8d66-a2662f4e" || true'
|
||||
- node tools/analytics/build-analytics success ci before_install
|
||||
|
||||
install:
|
||||
- ./scripts/ci/install.sh
|
||||
- node tools/analytics/build-analytics start ci install
|
||||
# Check the size of caches
|
||||
- du -sh ./node_modules || true
|
||||
# Install npm dependecies
|
||||
# check-node-modules will exit(1) if we don't need to install
|
||||
# we need to manually kick off the postinstall script if check-node-modules exit(0)s
|
||||
- node tools/npm/check-node-modules --purge && npm install || npm run postinstall
|
||||
- node tools/analytics/build-analytics success ci install
|
||||
|
||||
before_script:
|
||||
- node tools/analytics/build-analytics start ci before_script
|
||||
- mkdir -p $LOGS_DIR
|
||||
- ./scripts/ci/presubmit-queue-setup.sh
|
||||
- node tools/analytics/build-analytics success ci before_script
|
||||
|
||||
script:
|
||||
- ./scripts/ci/build.sh
|
||||
- ./scripts/ci/test.sh
|
||||
# deploy is part of 'script' and not 'after_success' so that we fail the build if the deployment fails
|
||||
- ./scripts/ci/deploy.sh
|
||||
- ./scripts/ci/angular.sh
|
||||
# all the scripts under this line will not quickly abort in case ${TRAVIS_TEST_RESULT} is 1 (job failure)
|
||||
- ./scripts/ci/cleanup.sh
|
||||
- ./scripts/ci/print-logs.sh
|
||||
- node tools/analytics/build-analytics start ci script
|
||||
- ./scripts/ci/build_and_test.sh ${MODE}
|
||||
- node tools/analytics/build-analytics success ci script
|
||||
|
||||
after_script:
|
||||
- node tools/analytics/build-analytics start ci after_script
|
||||
- ./scripts/ci/print-logs.sh
|
||||
- ./scripts/ci/after-script.sh
|
||||
- ./scripts/publish/publish-build-artifacts.sh
|
||||
- node tools/analytics/build-analytics success ci after_script
|
||||
- if [[ $TRAVIS_TEST_RESULT -eq 0 ]]; then node tools/analytics/build-analytics success ci job; else node tools/analytics/build-analytics error ci job; fi
|
||||
|
||||
notifications:
|
||||
webhooks:
|
||||
urls:
|
||||
- https://webhooks.gitter.im/e/1ef62e23078036f9cee4
|
||||
# trigger Buildtime Trend Service to parse Travis CI log
|
||||
- https://buildtimetrend.herokuapp.com/travis
|
||||
on_success: always # options: [always|never|change] default: always
|
||||
on_failure: always # options: [always|never|change] default: always
|
||||
on_start: false # default: false
|
||||
slack:
|
||||
secure: EP4MzZ8JMyNQJ4S3cd5LEPWSMjC7ZRdzt3veelDiOeorJ6GwZfCDHncR+4BahDzQAuqyE/yNpZqaLbwRWloDi15qIUsm09vgl/1IyNky1Sqc6lEknhzIXpWSalo4/T9ZP8w870EoDvM/UO+LCV99R3wS8Nm9o99eLoWVb2HIUu0=
|
||||
|
||||
|
3415
CHANGELOG.md
3415
CHANGELOG.md
File diff suppressed because it is too large
Load Diff
34
COMMITTER.md
Normal file
34
COMMITTER.md
Normal file
@ -0,0 +1,34 @@
|
||||
# Pushing changes into the Angular 2 tree
|
||||
|
||||
Please see [Using git with Angular repositories](https://docs.google.com/document/d/1h8nijFSaa1jG_UE8v4WP7glh5qOUXnYtAtJh_gwOQHI/edit)
|
||||
for details about how we maintain a linear commit history, and the rules for committing.
|
||||
|
||||
As a contributor, just read the instructions in [CONTRIBUTING.md](CONTRIBUTING.md) and send a pull request.
|
||||
Someone with committer access will do the rest.
|
||||
|
||||
## The `PR: merge` label and `presubmit-*` branches
|
||||
|
||||
We have automated the process for merging pull requests into master. Our goal is to minimize the disruption for
|
||||
Angular committers and also prevent breakages on master.
|
||||
|
||||
When a PR has `pr_state: LGTM` and is ready to merge, you should add the `pr_action: merge` label.
|
||||
Currently (late 2015), we need to ensure that each PR will cleanly merge into the Google-internal version control,
|
||||
so the caretaker reviews the changes manually.
|
||||
|
||||
After this review, the caretaker adds `zomg_admin: do_merge` which is restricted to admins only.
|
||||
A robot running as [mary-poppins](https://github.com/mary-poppins)
|
||||
is notified that the label was added by an authorized person,
|
||||
and will create a new branch in the angular project, using the convention `presubmit-{username}-pr-{number}`.
|
||||
|
||||
(Note: if the automation fails, committers can instead push the commits to a branch following this naming scheme.)
|
||||
|
||||
When a Travis build succeeds for a presubmit branch named following the convention,
|
||||
Travis will re-base the commits, merge to master, and close the PR automatically.
|
||||
|
||||
Finally, after merge `mary-poppins` removes the presubmit branch.
|
||||
|
||||
## Administration
|
||||
|
||||
The list of users who can trigger a merge by adding the `zomg_admin: do_merge` label is stored in our appengine app datastore.
|
||||
Edit the contents of the [CoreTeamMember Table](
|
||||
https://console.developers.google.com/project/angular2-automation/datastore/query?queryType=KindQuery&namespace=&kind=CoreTeamMember)
|
232
CONTRIBUTING.md
Normal file
232
CONTRIBUTING.md
Normal file
@ -0,0 +1,232 @@
|
||||
# Contributing to Angular 2
|
||||
|
||||
We would love for you to contribute to Angular 2 and help make it even better than it is
|
||||
today! As a contributor, here are the guidelines we would like you to follow:
|
||||
|
||||
- [Code of Conduct](#coc)
|
||||
- [Question or Problem?](#question)
|
||||
- [Issues and Bugs](#issue)
|
||||
- [Feature Requests](#feature)
|
||||
- [Submission Guidelines](#submit)
|
||||
- [Coding Rules](#rules)
|
||||
- [Commit Message Guidelines](#commit)
|
||||
- [Signing the CLA](#cla)
|
||||
|
||||
## <a name="coc"></a> Code of Conduct
|
||||
Help us keep Angular open and inclusive. Please read and follow our [Code of Conduct][coc].
|
||||
|
||||
## <a name="question"></a> Got a Question or Problem?
|
||||
|
||||
If you have questions about how to *use* Angular, please direct them to the [Google Group][angular-group]
|
||||
discussion list or [StackOverflow][stackoverflow]. Please note that Angular 2 is still in early developer preview, and the core team's capacity to answer usage questions is limited. We are also available on [Gitter][gitter].
|
||||
|
||||
## <a name="issue"></a> Found an Issue?
|
||||
If you find a bug in the source code or a mistake in the documentation, you can help us by
|
||||
[submitting an issue](#submit-issue) to our [GitHub Repository][github]. Even better, you can
|
||||
[submit a Pull Request](#submit-pr) with a fix.
|
||||
|
||||
## <a name="feature"></a> Want a Feature?
|
||||
You can *request* a new feature by [submitting an issue](#submit-issue) to our [GitHub
|
||||
Repository][github]. If you would like to *implement* a new feature, please submit an issue with
|
||||
a proposal for your work first, to be sure that we can use it. Angular 2 is in developer preview
|
||||
and we are not ready to accept major contributions ahead of the full release.
|
||||
Please consider what kind of change it is:
|
||||
|
||||
* For a **Major Feature**, first open an issue and outline your proposal so that it can be
|
||||
discussed. This will also allow us to better coordinate our efforts, prevent duplication of work,
|
||||
and help you to craft the change so that it is successfully accepted into the project.
|
||||
* **Small Features** can be crafted and directly [submitted as a Pull Request](#submit-pr).
|
||||
|
||||
## <a name="submit"></a> Submission Guidelines
|
||||
|
||||
### <a name="submit-issue"></a> Submitting an Issue
|
||||
Before you submit an issue, search the archive, maybe your question was already answered.
|
||||
|
||||
If your issue appears to be a bug, and hasn't been reported, open a new issue.
|
||||
Help us to maximize the effort we can spend fixing issues and adding new
|
||||
features, by not reporting duplicate issues. Providing the following information will increase the
|
||||
chances of your issue being dealt with quickly:
|
||||
|
||||
* **Overview of the Issue** - if an error is being thrown a non-minified stack trace helps
|
||||
* **Angular Version** - what version of Angular is affected (e.g. 2.0.0-alpha.53)
|
||||
* **Motivation for or Use Case** - explain what are you trying to do and why the current behavior is a bug for you
|
||||
* **Browsers and Operating System** - is this a problem with all browsers?
|
||||
* **Reproduce the Error** - provide a live example (using [Plunker][plunker],
|
||||
[JSFiddle][jsfiddle] or [Runnable][runnable]) or a unambiguous set of steps
|
||||
* **Related Issues** - has a similar issue been reported before?
|
||||
* **Suggest a Fix** - if you can't fix the bug yourself, perhaps you can point to what might be
|
||||
causing the problem (line of code or commit)
|
||||
|
||||
You can file new issues by providing the above information [here](https://github.com/angular/angular/issues/new).
|
||||
|
||||
|
||||
### <a name="submit-pr"></a> Submitting a Pull Request (PR)
|
||||
Before you submit your Pull Request (PR) consider the following guidelines:
|
||||
|
||||
* Search [GitHub](https://github.com/angular/angular/pulls) for an open or closed PR
|
||||
that relates to your submission. You don't want to duplicate effort.
|
||||
* Please sign our [Contributor License Agreement (CLA)](#cla) before sending PRs.
|
||||
We cannot accept code without this.
|
||||
* Make your changes in a new git branch:
|
||||
|
||||
```shell
|
||||
git checkout -b my-fix-branch master
|
||||
```
|
||||
|
||||
* Create your patch, **including appropriate test cases**.
|
||||
* Follow our [Coding Rules](#rules).
|
||||
* Run the full Angular test suite, as described in the [developer documentation][dev-doc],
|
||||
and ensure that all tests pass.
|
||||
* Commit your changes using a descriptive commit message that follows our
|
||||
[commit message conventions](#commit). Adherence to these conventions
|
||||
is necessary because release notes are automatically generated from these messages.
|
||||
|
||||
```shell
|
||||
git commit -a
|
||||
```
|
||||
Note: the optional commit `-a` command line option will automatically "add" and "rm" edited files.
|
||||
|
||||
* Push your branch to GitHub:
|
||||
|
||||
```shell
|
||||
git push origin my-fix-branch
|
||||
```
|
||||
|
||||
* In GitHub, send a pull request to `angular:master`.
|
||||
* If we suggest changes then:
|
||||
* Make the required updates.
|
||||
* Re-run the Angular 2 test suites for JS and Dart to ensure tests are still passing.
|
||||
* Rebase your branch and force push to your GitHub repository (this will update your Pull Request):
|
||||
|
||||
```shell
|
||||
git rebase master -i
|
||||
git push -f
|
||||
```
|
||||
|
||||
That's it! Thank you for your contribution!
|
||||
|
||||
#### After your pull request is merged
|
||||
|
||||
After your pull request is merged, you can safely delete your branch and pull the changes
|
||||
from the main (upstream) repository:
|
||||
|
||||
* Delete the remote branch on GitHub either through the GitHub web UI or your local shell as follows:
|
||||
|
||||
```shell
|
||||
git push origin --delete my-fix-branch
|
||||
```
|
||||
|
||||
* Check out the master branch:
|
||||
|
||||
```shell
|
||||
git checkout master -f
|
||||
```
|
||||
|
||||
* Delete the local branch:
|
||||
|
||||
```shell
|
||||
git branch -D my-fix-branch
|
||||
```
|
||||
|
||||
* Update your master with the latest upstream version:
|
||||
|
||||
```shell
|
||||
git pull --ff upstream master
|
||||
```
|
||||
|
||||
## <a name="rules"></a> Coding Rules
|
||||
To ensure consistency throughout the source code, keep these rules in mind as you are working:
|
||||
|
||||
* All features or bug fixes **must be tested** by one or more specs (unit-tests).
|
||||
* All public API methods **must be documented**. (Details TBC).
|
||||
* We follow [Google's JavaScript Style Guide][js-style-guide], but wrap all code at
|
||||
**100 characters**. An automated formatter is available, see
|
||||
[DEVELOPER.md](DEVELOPER.md#clang-format).
|
||||
|
||||
## <a name="commit"></a> Commit Message Guidelines
|
||||
|
||||
We have very precise rules over how our git commit messages can be formatted. This leads to **more
|
||||
readable messages** that are easy to follow when looking through the **project history**. But also,
|
||||
we use the git commit messages to **generate the Angular change log**.
|
||||
|
||||
### Commit Message Format
|
||||
Each commit message consists of a **header**, a **body** and a **footer**. The header has a special
|
||||
format that includes a **type**, a **scope** and a **subject**:
|
||||
|
||||
```
|
||||
<type>(<scope>): <subject>
|
||||
<BLANK LINE>
|
||||
<body>
|
||||
<BLANK LINE>
|
||||
<footer>
|
||||
```
|
||||
|
||||
The **header** is mandatory and the **scope** of the header is optional.
|
||||
|
||||
Any line of the commit message cannot be longer 100 characters! This allows the message to be easier
|
||||
to read on GitHub as well as in various git tools.
|
||||
|
||||
### Revert
|
||||
If the commit reverts a previous commit, it should begin with `revert: `, followed by the header of the reverted commit. In the body it should say: `This reverts commit <hash>.`, where the hash is the SHA of the commit being reverted.
|
||||
|
||||
### Type
|
||||
Must be one of the following:
|
||||
|
||||
* **feat**: A new feature
|
||||
* **fix**: A bug fix
|
||||
* **docs**: Documentation only changes
|
||||
* **style**: Changes that do not affect the meaning of the code (white-space, formatting, missing
|
||||
semi-colons, etc)
|
||||
* **refactor**: A code change that neither fixes a bug nor adds a feature
|
||||
* **perf**: A code change that improves performance
|
||||
* **test**: Adding missing tests or correcting existing tests
|
||||
* **build**: Changes that affect the build system, CI configuration or external dependencies (example scopes: gulp, broccoli, npm)
|
||||
* **ci**: Any changes to our CI configuration files and scripts (Travis, Circle CI, BrowserStack, SauceLabs)
|
||||
* **chore**: Other changes that don't modify `src` or `test` files
|
||||
|
||||
### Scope
|
||||
The scope could be anything specifying place of the commit change. For example
|
||||
`Compiler`, `ElementInjector`, etc.
|
||||
|
||||
### Subject
|
||||
The subject contains succinct description of the change:
|
||||
|
||||
* use the imperative, present tense: "change" not "changed" nor "changes"
|
||||
* don't capitalize first letter
|
||||
* no dot (.) at the end
|
||||
|
||||
### Body
|
||||
Just as in the **subject**, use the imperative, present tense: "change" not "changed" nor "changes".
|
||||
The body should include the motivation for the change and contrast this with previous behavior.
|
||||
|
||||
### Footer
|
||||
The footer should contain any information about **Breaking Changes** and is also the place to
|
||||
reference GitHub issues that this commit **Closes**.
|
||||
|
||||
**Breaking Changes** should start with the word `BREAKING CHANGE:` with a space or two newlines. The rest of the commit message is then used for this.
|
||||
|
||||
A detailed explanation can be found in this [document][commit-message-format].
|
||||
|
||||
## <a name="cla"></a> Signing the CLA
|
||||
|
||||
Please sign our Contributor License Agreement (CLA) before sending pull requests. For any code
|
||||
changes to be accepted, the CLA must be signed. It's a quick process, we promise!
|
||||
|
||||
* For individuals we have a [simple click-through form][individual-cla].
|
||||
* For corporations we'll need you to
|
||||
[print, sign and one of scan+email, fax or mail the form][corporate-cla].
|
||||
|
||||
|
||||
[angular-group]: https://groups.google.com/forum/#!forum/angular
|
||||
[coc]: https://github.com/angular/code-of-conduct/blob/master/CODE_OF_CONDUCT.md
|
||||
[commit-message-format]: https://docs.google.com/document/d/1QrDFcIiPjSLDn3EL15IJygNPiHORgU1_OOAqWjiDU5Y/edit#
|
||||
[corporate-cla]: http://code.google.com/legal/corporate-cla-v1.0.html
|
||||
[dev-doc]: https://github.com/angular/angular/blob/master/DEVELOPER.md
|
||||
[github]: https://github.com/angular/angular
|
||||
[gitter]: https://gitter.im/angular/angular
|
||||
[individual-cla]: http://code.google.com/legal/individual-cla-v1.0.html
|
||||
[js-style-guide]: http://google-styleguide.googlecode.com/svn/trunk/javascriptguide.xml
|
||||
[jsfiddle]: http://jsfiddle.net/
|
||||
[plunker]: http://plnkr.co/edit
|
||||
[runnable]: http://runnable.com/
|
||||
[stackoverflow]: http://stackoverflow.com/questions/tagged/angular
|
362
DEVELOPER.md
Normal file
362
DEVELOPER.md
Normal file
@ -0,0 +1,362 @@
|
||||
# Building and Testing Angular 2 for JS and Dart
|
||||
|
||||
This document describes how to set up your development environment to build and test Angular, both
|
||||
JS and Dart versions. It also explains the basic mechanics of using `git`, `node`, and `npm`.
|
||||
|
||||
* [Prerequisite Software](#prerequisite-software)
|
||||
* [Getting the Sources](#getting-the-sources)
|
||||
* [Environment Variable Setup](#environment-variable-setup)
|
||||
* [Installing NPM Modules and Dart Packages](#installing-npm-modules-and-dart-packages)
|
||||
* [Build commands](#build-commands)
|
||||
* [Running Tests Locally](#running-tests-locally)
|
||||
* [Code Style](#code-style)
|
||||
* [Project Information](#project-information)
|
||||
* [CI using Travis](#ci-using-travis)
|
||||
* [Transforming Dart code](#transforming-dart-code)
|
||||
* [Debugging](#debugging)
|
||||
|
||||
See the [contribution guidelines](https://github.com/angular/angular/blob/master/CONTRIBUTING.md)
|
||||
if you'd like to contribute to Angular.
|
||||
|
||||
## Prerequisite Software
|
||||
|
||||
Before you can build and test Angular, you must install and configure the
|
||||
following products on your development machine:
|
||||
|
||||
* [Git](http://git-scm.com) and/or the **GitHub app** (for [Mac](http://mac.github.com) or
|
||||
[Windows](http://windows.github.com)); [GitHub's Guide to Installing
|
||||
Git](https://help.github.com/articles/set-up-git) is a good source of information.
|
||||
|
||||
* [Node.js](http://nodejs.org), (version `>=5.4.1 <6`) which is used to run a development web server,
|
||||
run tests, and generate distributable files. We also use Node's Package Manager, `npm`
|
||||
(version `>=3.5.3 <4.0`), which comes with Node. Depending on your system, you can install Node either from
|
||||
source or as a pre-packaged bundle.
|
||||
|
||||
* *Optional*: [Dart](https://www.dartlang.org) (version ` >=1.13.2 <2.0.0`), specifically the Dart-SDK and
|
||||
Dartium (a version of [Chromium](http://www.chromium.org) with native support for Dart through
|
||||
the Dart VM). One of the **simplest** ways to get both is to install the **Dart Editor bundle**,
|
||||
which includes the editor, SDK and Dartium. See the [Dart tools](https://www.dartlang.org/tools)
|
||||
download [page for instructions](https://www.dartlang.org/tools/download.html).
|
||||
You can also download both **stable** and **dev** channel versions from the [download
|
||||
archive](https://www.dartlang.org/tools/download-archive). In that case, on Windows, Dart must be added
|
||||
to the `Path` (e.g. `path-to-dart-sdk-folder\bin`) and a new `DARTIUM_BIN` environment variable must be
|
||||
created, pointing to the executable (e.g. `path-to-dartium-folder\chrome.exe).`
|
||||
|
||||
|
||||
|
||||
## Getting the Sources
|
||||
|
||||
Fork and clone the Angular repository:
|
||||
|
||||
1. Login to your GitHub account or create one by following the instructions given
|
||||
[here](https://github.com/signup/free).
|
||||
2. [Fork](http://help.github.com/forking) the [main Angular
|
||||
repository](https://github.com/angular/angular).
|
||||
3. Clone your fork of the Angular repository and define an `upstream` remote pointing back to
|
||||
the Angular repository that you forked in the first place.
|
||||
|
||||
```shell
|
||||
# Clone your GitHub repository:
|
||||
git clone git@github.com:<github username>/angular.git
|
||||
|
||||
# Go to the Angular directory:
|
||||
cd angular
|
||||
|
||||
# Add the main Angular repository as an upstream remote to your repository:
|
||||
git remote add upstream https://github.com/angular/angular.git
|
||||
```
|
||||
|
||||
## Environment Variable Setup
|
||||
|
||||
Define the environment variables listed below. These are mainly needed for the testing. The
|
||||
notation shown here is for [`bash`](http://www.gnu.org/software/bash); adapt as appropriate for
|
||||
your favorite shell.
|
||||
|
||||
Examples given below of possible values for initializing the environment variables assume **Mac OS
|
||||
X** and that you have installed the Dart Editor in the directory named by
|
||||
`DART_EDITOR_DIR=/Applications/dart`. This is only for illustrative purposes.
|
||||
|
||||
```shell
|
||||
# DARTIUM_BIN: path to a Dartium browser executable; used by Karma to run Dart tests
|
||||
export DARTIUM_BIN="$DART_EDITOR_DIR/chromium/Chromium.app/Contents/MacOS/Chromium"
|
||||
```
|
||||
|
||||
Add the Dart SDK `bin` directory to your path and/or define `DART_SDK` (this is also detailed
|
||||
[here](https://www.dartlang.org/tools/pub/installing.html)):
|
||||
|
||||
```shell
|
||||
# DART_SDK: path to a Dart SDK directory
|
||||
export DART_SDK="$DART_EDITOR_DIR/dart-sdk"
|
||||
|
||||
# Update PATH to include the Dart SDK bin directory
|
||||
PATH+=":$DART_SDK/bin"
|
||||
```
|
||||
|
||||
And specify where the pub’s dependencies are downloaded. By default, this directory is located under .pub_cache
|
||||
in your home directory (on Mac and Linux), or in AppData\Roaming\Pub\Cache (on Windows).
|
||||
|
||||
```shell
|
||||
# PUB_CACHE: location of pub dependencies
|
||||
export PUB_CACHE="/Users/<user>/.pub-cache"
|
||||
```
|
||||
|
||||
## Installing NPM Modules and Dart Packages
|
||||
|
||||
Next, install the JavaScript modules and Dart packages needed to build and test Angular:
|
||||
|
||||
```shell
|
||||
# Install Angular project dependencies (package.json)
|
||||
npm install
|
||||
```
|
||||
|
||||
**Optional**: In this document, we make use of project local `npm` package scripts and binaries
|
||||
(stored under `./node_modules/.bin`) by prefixing these command invocations with `$(npm bin)`; in
|
||||
particular `gulp` and `protractor` commands. If you prefer, you can drop this path prefix by either:
|
||||
|
||||
*Option 1*: globally installing these two packages as follows:
|
||||
|
||||
* `npm install -g gulp` (you might need to prefix this command with `sudo`)
|
||||
* `npm install -g protractor` (you might need to prefix this command with `sudo`)
|
||||
|
||||
Since global installs can become stale, and required versions can vary by project, we avoid their
|
||||
use in these instructions.
|
||||
|
||||
*Option 2*: defining a bash alias like `alias nbin='PATH=$(npm bin):$PATH'` as detailed in this
|
||||
[Stackoverflow answer](http://stackoverflow.com/questions/9679932/how-to-use-package-installed-locally-in-node-modules/15157360#15157360) and used like this: e.g., `nbin gulp build`.
|
||||
|
||||
## Build commands
|
||||
|
||||
To build Angular and prepare tests, run:
|
||||
|
||||
```shell
|
||||
$(npm bin)/gulp build
|
||||
```
|
||||
|
||||
Notes:
|
||||
* Results are put in the `dist` folder.
|
||||
* This will also run `pub get` for the subfolders in `modules` and run `dartanalyzer` for
|
||||
every file that matches `<module>/src/<module>.dart`, e.g. `di/src/di.dart`.
|
||||
|
||||
You can selectively build either the JS or Dart versions as follows:
|
||||
|
||||
* `$(npm bin)/gulp build.js`
|
||||
* `$(npm bin)/gulp build.dart`
|
||||
|
||||
To clean out the `dist` folder, run:
|
||||
|
||||
```shell
|
||||
$(npm bin)/gulp clean
|
||||
```
|
||||
|
||||
## Running Tests Locally
|
||||
|
||||
### Full test suite
|
||||
|
||||
* `npm test`: full test suite for both JS and Dart versions of Angular. These are the same tests
|
||||
that run on Travis.
|
||||
|
||||
You can selectively run either the JS or Dart versions as follows:
|
||||
|
||||
* `$(npm bin)/gulp test.all.js`
|
||||
* `$(npm bin)/gulp test.all.dart`
|
||||
|
||||
### Unit tests
|
||||
|
||||
You can run just the unit tests as follows:
|
||||
|
||||
* `$(npm bin)/gulp test.unit.js`: JS tests in a browser; runs in **watch mode** (i.e.
|
||||
watches the test files for changes and re-runs tests when files are updated).
|
||||
* `$(npm bin)/gulp test.unit.cjs`: JS tests in NodeJS; runs in **watch mode**.
|
||||
* `$(npm bin)/gulp test.unit.dart`: Dart tests in Dartium; runs in **watch mode**.
|
||||
|
||||
If you prefer running tests in "single-run" mode rather than watch mode use:
|
||||
|
||||
* `$(npm bin)/gulp test.unit.js/ci`
|
||||
* `$(npm bin)/gulp test.unit.cjs/ci`
|
||||
* `$(npm bin)/gulp test.unit.dart/ci`
|
||||
|
||||
The task updates the dist folder with transpiled code whenever a source or test file changes, and
|
||||
Karma is run against the new output.
|
||||
|
||||
**Note**: If you want to only run a single test you can alter the test you wish to run by changing
|
||||
`it` to `iit` or `describe` to `ddescribe`. This will only run that individual test and make it
|
||||
much easier to debug. `xit` and `xdescribe` can also be useful to exclude a test and a group of
|
||||
tests respectively.
|
||||
|
||||
**Note**: **watch mode** needs symlinks to work, so if you're using windows, ensure you have the
|
||||
rights to built them in your operating system.
|
||||
|
||||
### Unit tests with Sauce Labs or Browser Stack
|
||||
|
||||
First, in a terminal, create a tunnel with [Sauce Connect](https://docs.saucelabs.com/reference/sauce-connect/) or [Browser Stack Local](https://www.browserstack.com/local-testing#command-line), and valid credentials.
|
||||
|
||||
Then, in another terminal:
|
||||
- Define the credentials as environment variables, e.g.:
|
||||
```
|
||||
export SAUCE_USERNAME='my_user'; export SAUCE_ACCESS_KEY='my_key';
|
||||
export BROWSER_STACK_USERNAME='my_user'; export BROWSER_STACK_ACCESS_KEY='my_key';
|
||||
```
|
||||
- Then run `gulp test.unit.js.(sauce|browserstack) --browsers=option1,option2,..,optionN`
|
||||
The options are any mix of browsers and aliases which are defined in the [browser-providers.conf.js](https://github.com/angular/angular/blob/master/browser-providers.conf.js) file.
|
||||
They are case insensitive, and the `SL_` or `BS_` prefix must not be added for browsers.
|
||||
|
||||
Some examples of commands:
|
||||
```
|
||||
gulp test.unit.js.sauce --browsers=Safari8,ie11 //run in Sauce Labs with Safari 8 and IE11
|
||||
gulp test.unit.js.browserstack --browsers=Safari,IE //run in Browser Stack with Safari 7, Safari 8, Safari 9, IE 9, IE 10 and IE 11
|
||||
gulp test.unit.js.sauce --browsers=IOS,safari8,android5.1 //run in Sauce Labs with iOS 7, iOS 8, iOs 9, Safari 8 and Android 5.1
|
||||
```
|
||||
|
||||
### E2E tests
|
||||
|
||||
1. `$(npm bin)/gulp build.js.cjs` (builds benchpress and tests into `dist/js/cjs` folder).
|
||||
2. `$(npm bin)/gulp serve.js.prod serve.dart` (runs a local webserver).
|
||||
3. `$(npm bin)/protractor protractor-js.conf.js`: JS e2e tests.
|
||||
4. `$(npm bin)/protractor protractor-dart2js.conf.js`: dart2js e2e tests.
|
||||
|
||||
Angular specific command line options when running protractor:
|
||||
- `$(npm bin)/protractor protractor-{js|dart2js}-conf.js --ng-help`
|
||||
|
||||
### Performance tests
|
||||
|
||||
1. `$(npm bin)/gulp build.js.cjs` (builds benchpress and tests into `dist/js/cjs` folder)
|
||||
2. `$(npm bin)/gulp serve.js.prod serve.dart` (runs a local webserver)
|
||||
3. `$(npm bin)/protractor protractor-js.conf.js --benchmark`: JS performance tests
|
||||
4. `$(npm bin)/protractor protractor-dart2js.conf.js --benchmark`: dart2js performance tests
|
||||
|
||||
Angular specific command line options when running protractor (e.g. force gc, ...):
|
||||
`$(npm bin)/protractor protractor-{js|dart2js}-conf.js --ng-help`
|
||||
|
||||
## Code Style
|
||||
|
||||
### Formatting with <a name="clang-format">clang-format</a>
|
||||
|
||||
We use [clang-format](http://clang.llvm.org/docs/ClangFormat.html) to automatically enforce code
|
||||
style for our TypeScript code. This allows us to focus our code reviews more on the content, and
|
||||
less on style nit-picking. It also lets us encode our style guide in the `.clang-format` file in the
|
||||
repository, allowing many tools and editors to share our settings.
|
||||
|
||||
To check the formatting of your code, run
|
||||
|
||||
gulp check-format
|
||||
|
||||
Note that the continuous build on Travis runs `gulp enforce-format`. Unlike the `check-format` task,
|
||||
this will actually fail the build if files aren't formatted according to the style guide.
|
||||
|
||||
Your life will be easier if you include the formatter in your standard workflow. Otherwise, you'll
|
||||
likely forget to check the formatting, and waste time waiting for a build on Travis that fails due
|
||||
to some whitespace difference.
|
||||
|
||||
* Use `$(npm bin)/clang-format -i [file name]` to format a file (or multiple).
|
||||
* Use `gulp enforce-format` to check if your code is `clang-format` clean. This also gives
|
||||
you a command line to format your code.
|
||||
* `clang-format` also includes a git hook, run `git clang-format` to format all files you
|
||||
touched.
|
||||
* You can run this as a **git pre-commit hook** to automatically format your delta regions when you
|
||||
commit a change. In the angular repo, run
|
||||
|
||||
```
|
||||
$ echo -e '#!/bin/sh\nexec git clang-format' > .git/hooks/pre-commit
|
||||
$ chmod u+x !$
|
||||
```
|
||||
|
||||
* **WebStorm** can run clang-format on the current file.
|
||||
1. Under Preferences, open Tools > External Tools.
|
||||
1. Plus icon to Create Tool
|
||||
1. Fill in the form:
|
||||
- Name: clang-format
|
||||
- Description: Format
|
||||
- Synchronize files after execution: checked
|
||||
- Open console: not checked
|
||||
- Show in: Editor menu
|
||||
- Program: `$ProjectFileDir$/node_modules/.bin/clang-format`
|
||||
- Parameters: `-i -style=file $FilePath$`
|
||||
- Working directory: `$ProjectFileDir$`
|
||||
* `clang-format` integrations are also available for many popular editors (`vim`, `emacs`,
|
||||
`Sublime Text`, etc.).
|
||||
|
||||
### Linting
|
||||
|
||||
We use [tslint](https://github.com/palantir/tslint) for linting. See linting rules in [gulpfile](gulpfile.js). To lint, run
|
||||
|
||||
```shell
|
||||
$ gulp lint
|
||||
```
|
||||
|
||||
## Generating the API documentation
|
||||
|
||||
The following gulp task will generate the API docs in the `dist/angular.io/partials/api/angular2`:
|
||||
|
||||
```shell
|
||||
$(npm bin)/gulp docs/angular.io
|
||||
```
|
||||
|
||||
You can serve the generated documentation to check how it would render on [angular.io](https://angular.io/):
|
||||
- check out the [angular.io repo](https://github.com/angular/angular.io) locally,
|
||||
- install dependencies as described in the [angular.io README](https://github.com/angular/angular.io/blob/master/README.md),
|
||||
- copy the generated documentation from your local angular repo at `angular/dist/angular.io/partials/api/angular2` to your local angular.io repo at `angular.io/public/docs/js/latest/api`,
|
||||
- run `harp compile` at the root of the angular.io repo to check the generated documentation for errors,
|
||||
- run `harp server` and open a browser at `http://localhost:9000/docs/js/latest/api/` to check the rendered documentation.
|
||||
|
||||
## Project Information
|
||||
|
||||
### Folder structure
|
||||
|
||||
* `modules/*`: modules that will be loaded in the browser
|
||||
* `tools/*`: tools that are needed to build Angular
|
||||
* `dist/*`: build files are placed here.
|
||||
|
||||
### File suffixes
|
||||
|
||||
* `*.ts`: TypeScript files that get transpiled to Dart and EcmaScript 5/6
|
||||
* `*.dart`: Dart files that don't get transpiled
|
||||
|
||||
## CI using Travis
|
||||
|
||||
For instructions on setting up Continuous Integration using Travis, see the instructions given
|
||||
[here](https://github.com/angular/angular.dart/blob/master/travis.md).
|
||||
|
||||
## Transforming Dart code
|
||||
|
||||
See the [wiki](//github.com/angular/angular/wiki/Angular-2-Dart-Transformer).
|
||||
|
||||
## Debugging
|
||||
|
||||
### Debug the transpiler
|
||||
|
||||
If you need to debug the transpiler:
|
||||
|
||||
- add a `debugger;` statement in the transpiler code,
|
||||
- from the root folder, execute `node debug $(npm bin)/gulp build` to enter the node
|
||||
debugger
|
||||
- press "c" to execute the program until you reach the `debugger;` statement,
|
||||
- you can then type "repl" to enter the REPL and inspect variables in the context.
|
||||
|
||||
See the [Node.js manual](http://nodejs.org/api/debugger.html) for more information.
|
||||
|
||||
Notes:
|
||||
- You can also execute `node $(npm bin)/karma start karma-dart.conf.js` depending on which
|
||||
code you want to debug (the former will process the "modules" folder while the later processes
|
||||
the transpiler specs).
|
||||
- You can also add `debugger;` statements in the specs (JavaScript). The execution will halt when
|
||||
the developer tools are opened in the browser running Karma.
|
||||
|
||||
### Debug the tests
|
||||
|
||||
If you need to debug the tests:
|
||||
|
||||
- add a `debugger;` statement to the test you want to debug (or the source code),
|
||||
- execute karma `$(npm bin)/gulp test.js`,
|
||||
- press the top right "DEBUG" button,
|
||||
- open the DevTools and press F5,
|
||||
- the execution halts at the `debugger;` statement
|
||||
|
||||
**Note (WebStorm users)**:
|
||||
|
||||
1. Create a Karma run config from WebStorm.
|
||||
2. Then in the "Run" menu, press "Debug 'karma-js.conf.js'", and WebStorm will stop in the generated
|
||||
code on the `debugger;` statement.
|
||||
3. You can then step into the code and add watches.
|
||||
|
||||
The `debugger;` statement is needed because WebStorm will stop in a transpiled file. Breakpoints in
|
||||
the original source files are not supported at the moment.
|
2
LICENSE
2
LICENSE
@ -1,6 +1,6 @@
|
||||
The MIT License
|
||||
|
||||
Copyright (c) 2014-2017 Google, Inc. http://angular.io
|
||||
Copyright (c) 2014-2016 Google, Inc. http://angular.io
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
@ -1,7 +1,7 @@
|
||||
Naming Conventions in Angular
|
||||
Naming Conventions in Angular2
|
||||
---
|
||||
|
||||
In general Angular should follow TypeScript naming conventions.
|
||||
In general Angular2 should follow TypeScript naming conventions.
|
||||
See: https://github.com/Microsoft/TypeScript/wiki/Coding-guidelines
|
||||
|
||||
|
26
README.md
26
README.md
@ -1,18 +1,18 @@
|
||||
[](https://travis-ci.org/angular/angular)
|
||||
[](https://circleci.com/gh/angular/angular/tree/master)
|
||||
[](https://travis-ci.org/angular/angular)
|
||||
[](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)
|
||||
[](https://badge.fury.io/js/%40angular%2Fcore)
|
||||
|
||||
[](http://issuestats.com/github/angular/angular)
|
||||
[](http://issuestats.com/github/angular/angular)
|
||||
[](http://badge.fury.io/js/angular2)
|
||||
[](https://npmjs.org/package/angular2)
|
||||
[](https://saucelabs.com/u/angular2-ci)
|
||||
*Safari (7+), iOS (7+), Edge (14) and IE mobile (11) are tested on [BrowserStack][browserstack].*
|
||||
|
||||
Angular
|
||||
Angular
|
||||
=========
|
||||
|
||||
Angular is a development platform for building mobile and desktop web applications using Typescript/JavaScript (JS) and other languages.
|
||||
Angular is a development platform for building mobile and desktop web applications. This is the
|
||||
repository for [Angular 2][ng2], both the JavaScript (JS) and [Dart][dart] versions.
|
||||
|
||||
Angular 2 is currently in **Beta**.
|
||||
|
||||
## Quickstart
|
||||
|
||||
@ -24,7 +24,11 @@ Angular is a development platform for building mobile and desktop web applicatio
|
||||
Want to file a bug, contribute some code, or improve documentation? Excellent! Read up on our
|
||||
guidelines for [contributing][contributing] and then check out one of our issues in the [hotlist: community-help](https://github.com/angular/angular/labels/hotlist%3A%20community-help).
|
||||
|
||||
[browserstack]: https://www.browserstack.com/
|
||||
|
||||
[contributing]: http://github.com/angular/angular/blob/master/CONTRIBUTING.md
|
||||
[dart]: http://www.dartlang.org
|
||||
[dartium]: http://www.dartlang.org/tools/dartium
|
||||
[quickstart]: https://angular.io/docs/ts/latest/quickstart.html
|
||||
[ng]: http://angular.io
|
||||
[ng2]: http://angular.io
|
||||
[ngDart]: http://angulardart.org
|
||||
[ngJS]: http://angularjs.org
|
||||
|
4
TOOLS.md
Normal file
4
TOOLS.md
Normal file
@ -0,0 +1,4 @@
|
||||
# Developer Tools for Angular 2
|
||||
|
||||
- [JavaScript](TOOLS_JS.md)
|
||||
- [Dart](TOOLS_DART.md)
|
376
TOOLS_DART.md
Normal file
376
TOOLS_DART.md
Normal file
@ -0,0 +1,376 @@
|
||||
# Developer Tools for Dart
|
||||
|
||||
Use these tools and techniques to increase your app's performance
|
||||
and reliability.
|
||||
|
||||
* [Angular debugging tools](#angular-debugging-tools)
|
||||
* [Code size](#code-size)
|
||||
* [Performance](#performance)
|
||||
|
||||
|
||||
## Angular debugging tools
|
||||
|
||||
Starting with alpha.38, Angular provides a set of debugging tools
|
||||
that are accessible from any browser's developer console.
|
||||
In Chrome, you can get to the dev console by pressing
|
||||
Ctrl + Shift + J (on Mac: Cmd + Opt + J).
|
||||
|
||||
### Enabling the debugging tools
|
||||
|
||||
By default the debugging tools are disabled.
|
||||
Enable the debugging tools as follows:
|
||||
|
||||
```dart
|
||||
import 'package:angular2/platform/browser.dart';
|
||||
|
||||
main() async {
|
||||
var appRef = await bootstrap(Application);
|
||||
enableDebugTools(appRef);
|
||||
}
|
||||
```
|
||||
|
||||
<!-- Change function name to enableDebuggingTools? -->
|
||||
|
||||
|
||||
### Using the debugging tools
|
||||
|
||||
In the browser, open the dev console. The top-level object is called `ng` and
|
||||
contains more specific tools inside it.
|
||||
|
||||
For example, to run the change detection profiler on your app:
|
||||
|
||||
```javascript
|
||||
// In the dev console:
|
||||
ng.profiler.timeChangeDetection();
|
||||
```
|
||||
|
||||
The [Change detection profiler](#change-detection-profiler) section
|
||||
has more details.
|
||||
<!-- Point to API docs when they're published, if they're useful.
|
||||
They should be under
|
||||
http://www.dartdocs.org/documentation/angular2/latest
|
||||
and/or
|
||||
https://angular.io/docs/js/latest/api/. -->
|
||||
|
||||
|
||||
## Code size
|
||||
|
||||
Code must be downloaded, parsed, and executed. Too much code can lead to
|
||||
slow application start-up time, especially on slow networks and low-end devices.
|
||||
The tools and techniques in this section can help you to identify
|
||||
unnecessarily large code and to reduce code size.
|
||||
|
||||
### Finding contributors to code size
|
||||
|
||||
Options for investigating code size include the `--dump-info` dart2js option,
|
||||
ng2soyc, `reflector.trackUsage()`, and code coverage information
|
||||
from the Dart VM.
|
||||
|
||||
#### Use --dump-info
|
||||
|
||||
The `--dump-info` option of `dart2js` outputs information about what happened
|
||||
during compilation. You can specify `--dump-info` in `pubspec.yaml`:
|
||||
|
||||
```yaml
|
||||
transformers:
|
||||
...
|
||||
- $dart2js:
|
||||
commandLineOptions:
|
||||
- --dump-info
|
||||
```
|
||||
|
||||
The [Dump Info Visualizer](https://github.com/dart-lang/dump-info-visualizer)
|
||||
can help you analyze the output.
|
||||
For more information, see the
|
||||
[dart2js_info API reference](http://dart-lang.github.io/dart2js_info/doc/api/).
|
||||
|
||||
#### Use ng2soyc.dart
|
||||
|
||||
[ng2soyc](https://github.com/angular/ng2soyc.dart) is a utility for analyzing
|
||||
code size contributors in Angular 2 applications. It groups code size by
|
||||
library and, assuming your library names follow
|
||||
[standard naming conventions](https://www.dartlang.org/articles/style-guide/#do-prefix-library-names-with-the-package-name-and-a-dot-separated-path)
|
||||
(package.library.sublibrary...), gives the code size breakdown at
|
||||
each level. To reduce noise in the output of very large apps, ng2soyc provides
|
||||
an option to hide libraries that are too small, so you can focus on the biggest
|
||||
contributors.
|
||||
|
||||
#### Find unused reflection data
|
||||
|
||||
Your app might have types that are annotated with `@Component` or `@Injectable`
|
||||
but never used.
|
||||
To find these unused types, use `reflector.trackUsage()` and then,
|
||||
after exercising your app, `reflector.listUnusedKeys()`.
|
||||
For example:
|
||||
|
||||
```
|
||||
import 'package:angular2/src/core/reflection/reflection.dart';
|
||||
...
|
||||
main() async {
|
||||
reflector.trackUsage();
|
||||
await bootstrap(AppComponent);
|
||||
print('Unused keys: ${reflector.listUnusedKeys()}');
|
||||
}
|
||||
```
|
||||
|
||||
When you run that code (in Dartium or another browser),
|
||||
you'll see a list of types that Angular _can_ inject but hasn't needed to.
|
||||
Consider removing those types or their `@Component`/`@Injectable` annotation
|
||||
to decrease your app's code size.
|
||||
|
||||
Three conditions must be true for `listUnusedKeys()` to return helpful data:
|
||||
|
||||
1. The angular2 transformer must run on the app.
|
||||
2. If you're running a JavaScript version of the app,
|
||||
the app must not be minified, so that the names are readable.
|
||||
3. You must exercise your app in as many ways as possible
|
||||
before calling `listUnusedKeys()`.
|
||||
Otherwise, you might get false positives:
|
||||
keys that haven't been used only because you didn't exercise
|
||||
the relevant feature of the app.
|
||||
|
||||
To run the angular2 transformer, first specify it in `pubspec.yaml`:
|
||||
|
||||
```
|
||||
name: hello_world
|
||||
...
|
||||
transformers:
|
||||
- angular2:
|
||||
entry_points: web/main.dart
|
||||
```
|
||||
|
||||
Then use pub to run the transformer. If you use `pub serve`,
|
||||
it provides both Dart and unminified (by default) JavaScript versions.
|
||||
If you want to serve actual files, then use `pub build` in debug mode
|
||||
to generate Dart and unminified JavaScript files:
|
||||
`pub build --mode=debug`.
|
||||
|
||||
The `reflector.trackUsage()` method makes Angular track the reflection
|
||||
information used by the app. Reflection information (`ReflectionInfo`) is a data
|
||||
structure that stores information that Angular uses for locating DI factories
|
||||
and for generating change detectors and other code related to a
|
||||
given type.
|
||||
|
||||
#### Use code coverage to find dead code
|
||||
|
||||
When running in Dartium (or in the Dart VM, in general) you can request code
|
||||
coverage information from the VM. You can either use
|
||||
[observatory](https://www.dartlang.org/tools/observatory/) or download
|
||||
the coverage file and use your own tools to inspect it. Lines of code that are
|
||||
not covered are top candidates for dead code.
|
||||
|
||||
Keep in mind, however, that uncovered code is not sufficient evidence of dead
|
||||
code, only necessary evidence. It is perfectly possible that you simply didn't
|
||||
exercise your application in a way that triggers the execution of uncovered
|
||||
code. A common example is error handling code. Just because your testing never
|
||||
encountered an error does not mean the error won't happen in production. You
|
||||
therefore don't have to rush and remove all the `catch` blocks.
|
||||
|
||||
### Reducing code size
|
||||
|
||||
To reduce code size, you can disable reflection,
|
||||
enable minification, and manually remove dead code.
|
||||
You can also try less safe options such as
|
||||
telling dart2js to trust type annotations.
|
||||
|
||||
|
||||
#### Disable reflection
|
||||
|
||||
`dart:mirrors` allows discovering program metadata at runtime. However, this
|
||||
means that `dart2js` needs to retain that metadata and thus increase the size
|
||||
of resulting JS output. In practice, however, it is possible to extract most
|
||||
metadata necessary for your metaprogramming tasks statically using a
|
||||
transformer and `package:analyzer`, and act on it before compiling to JS.
|
||||
|
||||
#### Enable minification
|
||||
|
||||
Minification shortens all your `longMethodNames` into 2- or 3-letter long
|
||||
symbols. `dart2js` ensures that this kind of renaming is done safely, without
|
||||
breaking the functionality of your programs. You can enable it in `pubspec.yaml`
|
||||
under `$dart2js` transformer:
|
||||
|
||||
```yaml
|
||||
transformers:
|
||||
...
|
||||
- $dart2js:
|
||||
minify: true
|
||||
```
|
||||
|
||||
#### Manually remove dead code
|
||||
|
||||
`dart2js` comes with dead code elimination out-of-the-box. However, it may not
|
||||
always be able to tell if a piece of code could be used. Consider the following
|
||||
example:
|
||||
|
||||
```dart
|
||||
/// This function decides which serialization format to use
|
||||
void setupSerializers() {
|
||||
if (server.doYouSupportProtocolBuffers()) {
|
||||
useProtobufSerializers();
|
||||
} else {
|
||||
useJsonSerializers();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
In this example the application asks the server what kind of serialization
|
||||
format it uses and dynamically chooses one or the other. `dart2js` can't
|
||||
tell whether the server responds with yes or no, so it must retain both
|
||||
kinds of serializers. However, if you know that your server supports
|
||||
protocol buffers, you can remove that `if` block entirely and default to
|
||||
protocol buffers.
|
||||
|
||||
Code coverage (see above) is a good way to find dead code in your app.
|
||||
|
||||
#### Unsafe options
|
||||
|
||||
Dart also provides more aggressive optimization options. However, you have to
|
||||
be careful when using them and as of today the benefits aren't that clear. If
|
||||
your type annotations are inaccurate you may end up with non-Darty runtime
|
||||
behavior, including the classic "undefined is not a function" tautology, as
|
||||
well as the "keep on truckin'" behavior, e.g. `null + 1 == 1` and
|
||||
`{} + [] == 0`.
|
||||
|
||||
`--trust-type-annotations` tells `dart2js` to trust that your type annotations
|
||||
are correct. So if you have a function `foo(Bar bar)` the compiler can omit the
|
||||
check that `bar` is truly `Bar` when calling methods on it.
|
||||
|
||||
`--trust-primitives` tells `dart2js` that primitive types, such as numbers and
|
||||
booleans are never `null` when performing arithmetic, and that your program
|
||||
does not run into range error when operating on lists, letting the compiler
|
||||
remove some of the error checking code.
|
||||
|
||||
Specify these options in `pubspec.yaml`.
|
||||
|
||||
Example:
|
||||
|
||||
```yaml
|
||||
transformers:
|
||||
...
|
||||
- $dart2js:
|
||||
commandLineOptions:
|
||||
- --trust-type-annotations
|
||||
- --trust-primitives
|
||||
```
|
||||
|
||||
## Performance
|
||||
|
||||
### Change detection profiler
|
||||
|
||||
If your application is janky (it misses frames) or is slow according to other
|
||||
metrics, you need to find out why. This tool helps by measuring the average
|
||||
speed of _change detection_, a phase in Angular's
|
||||
lifecycle that detects changes in values that are bound to the UI.
|
||||
Janky UI updates can result from slowness either in _computing_ the changes or
|
||||
in _applying_ those changes to the UI.
|
||||
|
||||
For your app to be performant, the process of _computing_ changes must be very
|
||||
fast—preferably **under 3 milliseconds**.
|
||||
Fast change computation leaves room for
|
||||
the application logic, UI updates, and browser rendering pipeline
|
||||
to fit within a 16 ms frame (assuming a target frame rate of 60 FPS).
|
||||
|
||||
The change detection profiler repeatedly performs change detection
|
||||
without invoking any user actions, such as clicking buttons or entering
|
||||
text in input fields. It then computes the average amount of time
|
||||
(in milliseconds) to perform a single cycle of change detection and
|
||||
prints that to the console. This number depends on the current state of the UI. You are likely to see different numbers
|
||||
as you go from one screen in your application to another.
|
||||
|
||||
#### Running the profiler
|
||||
|
||||
Before running the profiler, enable the debugging tools
|
||||
and put the app into the state you want to measure:
|
||||
|
||||
1. If you haven't already done so,
|
||||
[enable the debugging tools](#enabling-the-debugging-tools).
|
||||
2. Navigate the app to a screen whose performance you want to profile.
|
||||
3. Make sure the screen is in a state that you want to measure.
|
||||
For example, you might want to profile the screen several times,
|
||||
with different amounts and kinds of data.
|
||||
|
||||
To run the profiler, enter the following in the dev console:
|
||||
|
||||
```javascript
|
||||
ng.profiler.timeChangeDetection();
|
||||
```
|
||||
|
||||
The results are visible in the console.
|
||||
|
||||
|
||||
#### Recording CPU profiles
|
||||
|
||||
To record a profile, pass `{record: true}` to `timeChangeDetection()`:
|
||||
|
||||
```javascript
|
||||
ng.profiler.timeChangeDetection({record: true});
|
||||
```
|
||||
|
||||
Then open the **Profiles** tab. The recorded profile has the title
|
||||
**Change Detection**. In Chrome, if you record the profile repeatedly, all the
|
||||
profiles are nested under Change Detection.
|
||||
|
||||
|
||||
#### Interpreting the numbers
|
||||
|
||||
In a properly designed application, repeated attempts to detect changes without
|
||||
any user actions result in no changes to the UI. It is
|
||||
also desirable to have the cost of a user action be proportional to the amount
|
||||
of UI changes required. For example, popping up a menu with 5 items should be
|
||||
vastly faster than rendering a table of 500 rows and 10 columns. Therefore,
|
||||
change detection with no UI updates should be as fast as possible.
|
||||
|
||||
#### Investigating slow change detection
|
||||
|
||||
So you found a screen in your application on which the profiler reports a very
|
||||
high number (i.e. >3ms). This is where a recorded CPU profile can help. Enable
|
||||
recording while profiling:
|
||||
|
||||
```javascript
|
||||
ng.profiler.timeChangeDetection({record: true});
|
||||
```
|
||||
|
||||
Then look for hot spots using
|
||||
[Chrome CPU profiler](https://developer.chrome.com/devtools/docs/cpu-profiling).
|
||||
|
||||
#### Reducing change detection cost
|
||||
|
||||
There are many reasons for slow change detection. To gain intuition about
|
||||
possible causes it helps to understand how change detection works. Such a
|
||||
discussion is outside the scope of this document,
|
||||
but here are some key concepts.
|
||||
|
||||
<!-- TODO: link to change detection docs -->
|
||||
|
||||
By default, Angular uses a _dirty checking_ mechanism to find model changes.
|
||||
This mechanism involves evaluating every bound expression that's active on the
|
||||
UI. These usually include text interpolation via `{{expression}}` and property
|
||||
bindings via `[prop]="expression"`. If any of the evaluated expressions are
|
||||
costly to compute, they might contribute to slow change detection. A good way to
|
||||
speed things up is to use plain class fields in your expressions and avoid any
|
||||
kind of computation. For example:
|
||||
|
||||
```dart
|
||||
@View(
|
||||
template: '<button [enabled]="isEnabled">{{title}}</button>'
|
||||
)
|
||||
class FancyButton {
|
||||
// GOOD: no computation, just returns the value
|
||||
bool isEnabled;
|
||||
|
||||
// BAD: computes the final value upon request
|
||||
String _title;
|
||||
String get title => _title.trim().toUpperCase();
|
||||
}
|
||||
```
|
||||
|
||||
Most cases like these can be solved by precomputing the value and storing the
|
||||
final value in a field.
|
||||
|
||||
Angular also supports a second type of change detection: the _push_ model. In
|
||||
this model, Angular does not poll your component for changes. Instead, the
|
||||
component tells Angular when it changes, and only then does Angular perform
|
||||
the update. This model is suitable in situations when your data model uses
|
||||
observable or immutable objects.
|
||||
|
||||
<!-- TODO: link to discussion of push model -->
|
140
TOOLS_JS.md
Normal file
140
TOOLS_JS.md
Normal file
@ -0,0 +1,140 @@
|
||||
# Developer Tools for JavaScript
|
||||
|
||||
Here you will find a collection of tools and tips for keeping your application
|
||||
perform well and contain fewer bugs.
|
||||
|
||||
## Angular debug tools in the dev console
|
||||
|
||||
Angular provides a set of debug tools that are accessible from any browser's
|
||||
developer console. In Chrome the dev console can be accessed by pressing
|
||||
Ctrl + Shift + j.
|
||||
|
||||
### Enabling debug tools
|
||||
|
||||
By default the debug tools are disabled. You can enable debug tools as follows:
|
||||
|
||||
```typescript
|
||||
import {enableDebugTools} from 'angular2/platform/browser';
|
||||
|
||||
bootstrap(Application).then((appRef) => {
|
||||
enableDebugTools(appRef);
|
||||
});
|
||||
```
|
||||
|
||||
### Using debug tools
|
||||
|
||||
In the browser open the developer console (Ctrl + Shift + j in Chrome). The
|
||||
top level object is called `ng` and contains more specific tools inside it.
|
||||
|
||||
Example:
|
||||
|
||||
```javascript
|
||||
ng.profiler.timeChangeDetection();
|
||||
```
|
||||
|
||||
## Performance
|
||||
|
||||
### Change detection profiler
|
||||
|
||||
If your application is janky (it misses frames) or is slow according to other
|
||||
metrics it is important to find the root cause of the issue. Change detection
|
||||
is a phase in Angular's lifecycle that detects changes in values that are
|
||||
bound to UI, and if it finds a change it performs the corresponding UI update.
|
||||
However, sometimes it is hard to tell if the slowness is due to the act of
|
||||
computing the changes being slow, or due to the act of applying those changes
|
||||
to the UI. For your application to be performant it is important that the
|
||||
process of computing changes is very fast. For best results it should be under
|
||||
3 milliseconds in order to leave room for the application logic, the UI updates
|
||||
and browser's rendering pipeline to fit withing the 16 millisecond frame
|
||||
(assuming the 60 FPS target frame rate).
|
||||
|
||||
Change detection profiler repeatedly performs change detection without invoking
|
||||
any user actions, such as clicking buttons or entering text in input fields. It
|
||||
then computes the average amount of time it took to perform a single cycle of
|
||||
change detection in milliseconds and prints it to the console. This number
|
||||
depends on the current state of the UI. You will likely see different numbers
|
||||
as you go from one screen in your application to another.
|
||||
|
||||
#### Running the profiler
|
||||
|
||||
Enable debug tools (see above), then in the dev console enter the following:
|
||||
|
||||
```javascript
|
||||
ng.profiler.timeChangeDetection();
|
||||
```
|
||||
|
||||
The results will be printed to the console.
|
||||
|
||||
#### Recording CPU profile
|
||||
|
||||
Pass `{record: true}` an argument:
|
||||
|
||||
```javascript
|
||||
ng.profiler.timeChangeDetection({record: true});
|
||||
```
|
||||
|
||||
Then open the "Profiles" tab. You will see the recorded profile titled
|
||||
"Change Detection". In Chrome, if you record the profile repeatedly, all the
|
||||
profiles will be nested under "Change Detection".
|
||||
|
||||
#### Interpreting the numbers
|
||||
|
||||
In a properly-designed application repeated attempts to detect changes without
|
||||
any user actions should result in no changes to be applied on the UI. It is
|
||||
also desirable to have the cost of a user action be proportional to the amount
|
||||
of UI changes required. For example, popping up a menu with 5 items should be
|
||||
vastly faster than rendering a table of 500 rows and 10 columns. Therefore,
|
||||
change detection with no UI updates should be as fast as possible. Ideally the
|
||||
number printed by the profiler should be well below the length of a single
|
||||
animation frame (16ms). A good rule of thumb is to keep it under 3ms.
|
||||
|
||||
#### Investigating slow change detection
|
||||
|
||||
So you found a screen in your application on which the profiler reports a very
|
||||
high number (i.e. >3ms). This is where a recorded CPU profile can help. Enable
|
||||
recording while profiling:
|
||||
|
||||
```javascript
|
||||
ng.profiler.timeChangeDetection({record: true});
|
||||
```
|
||||
|
||||
Then look for hot spots using
|
||||
[Chrome CPU profiler](https://developer.chrome.com/devtools/docs/cpu-profiling).
|
||||
|
||||
#### Reducing change detection cost
|
||||
|
||||
There are many reasons for slow change detection. To gain intuition about
|
||||
possible causes it would help to understand how change detection works. Such a
|
||||
discussion is outside the scope of this document (TODO link to docs), but here
|
||||
are some key concepts in brief.
|
||||
|
||||
By default Angular uses "dirty checking" mechanism for finding model changes.
|
||||
This mechanism involves evaluating every bound expression that's active on the
|
||||
UI. These usually include text interpolation via `{{expression}}` and property
|
||||
bindings via `[prop]="expression"`. If any of the evaluated expressions are
|
||||
costly to compute they could contribute to slow change detection. A good way to
|
||||
speed things up is to use plain class fields in your expressions and avoid any
|
||||
kinds of computation. Example:
|
||||
|
||||
```typescript
|
||||
@View({
|
||||
template: '<button [enabled]="isEnabled">{{title}}</button>'
|
||||
})
|
||||
class FancyButton {
|
||||
// GOOD: no computation, just return the value
|
||||
isEnabled: boolean;
|
||||
|
||||
// BAD: computes the final value upon request
|
||||
_title: String;
|
||||
get title(): String { return this._title.trim().toUpperCase(); }
|
||||
}
|
||||
```
|
||||
|
||||
Most cases like these could be solved by precomputing the value and storing the
|
||||
final value in a field.
|
||||
|
||||
Angular also supports a second type of change detection - the "push" model. In
|
||||
this model Angular does not poll your component for changes. Instead, the
|
||||
component "tells" Angular when it changes and only then does Angular perform
|
||||
the update. This model is suitable in situations when your data model uses
|
||||
observable or immutable objects (also a discussion for another time).
|
168
TRIAGE_AND_LABELS.md
Normal file
168
TRIAGE_AND_LABELS.md
Normal file
@ -0,0 +1,168 @@
|
||||
# Triage Process and Github Labels for Angular 2
|
||||
|
||||
This document describes how the Angular team uses labels and milestones to triage issues on github.
|
||||
|
||||
# Issues and PRs
|
||||
## Triaged vs Untriaged Issues
|
||||
|
||||
Every triaged issue must have four attributes assigned to it:
|
||||
|
||||
* `priority` -- P0 through P4. P0 issues are "drop everything and do this now". P4 are nice to have.
|
||||
* `component` -- Which area of Angular knowledge this relates to.
|
||||
* `effort` -- Rough assessment of how much work this issue is. E.g. `effort: easy` means
|
||||
"probably a few hours of work".
|
||||
* `type` -- Whether this issue is a bug, feature, or other kind of task.
|
||||
|
||||
Untriaged issues are any issues in the queue that don't yet have these four attributes.
|
||||
|
||||
You can view a report of untriaged issues here, in our
|
||||
[Angular Triage Dashboard](http://mhevery.github.io/github_issues/).
|
||||
|
||||
Issues should also have a clear action to complete that can be addressed or resolved within the
|
||||
scope of Angular 2. We'll close issues that don't meet these criteria.
|
||||
|
||||
### Assigning Issues to Milestones
|
||||
|
||||
Any issue that is being worked on must have:
|
||||
|
||||
* An `assignee`: The person doing the work.
|
||||
* A `Milestone`: When we expect to complete this work.
|
||||
|
||||
We aim to only have at most three milestones open at a time:
|
||||
|
||||
* Closing Milestone: A milestone with a very small number of issues, about to release.
|
||||
* Current Milestone: Work that we plan to complete within one week.
|
||||
* Next Milestone: Work that is > 1 week but current for the team.
|
||||
|
||||
The [backlog](https://github.com/angular/angular/issues?q=is%3Aopen+is%3Aissue+no%3Amilestone)
|
||||
consists of all issues that have been triaged but do not have an assignee or milestone.
|
||||
|
||||
## Triaged vs Untriaged PRs
|
||||
|
||||
Because of the cumulative pain associated with rebasing PRs, we triage PRs daily, and
|
||||
closing or reviewing PRs is a top priority ahead of other ongoing work.
|
||||
|
||||
Every triaged PR must have a `pr_action` label assigned to it and an assignee:
|
||||
|
||||
* `pr_action: review` -- work is complete and comment is needed from the assignee.
|
||||
* `pr_action: cleanup` -- more work is needed from the current assignee.
|
||||
* `pr_action: discuss` -- discussion is needed, to be led by the current assignee.
|
||||
* `pr_action: merge` -- the PR should be merged. Add this to a PR when you would like to
|
||||
trigger automatic merging following a successful build. This is described in [COMMITTER.md](COMMITTER.md).
|
||||
|
||||
In addition, PRs can have the following states:
|
||||
|
||||
* `pr_state: LGTM` -- PR may have outstanding changes but does not require further review.
|
||||
* `pr_state: WIP` -- PR is experimental or rapidly changing. Not ready for review or triage.
|
||||
* `pr_state: blocked` -- PR is blocked on an issue or other PR. Not ready for review or triage.
|
||||
|
||||
Note that an LGTM state does not mean a PR is ready to merge: for example, a reviewer might set the
|
||||
LGTM state but request a minor tweak that doesn't need further review, e.g., a rebase or small
|
||||
uncontroversial change.
|
||||
|
||||
PRs do not need to be assigned to milestones, unless a milestone release should be held for that
|
||||
PR to land.
|
||||
|
||||
Victor (`vsavkin`) and Tobias (`tbosch`) are owners of the PR queue. Here is a list of [current
|
||||
untriaged PRs](https://github.com/angular/angular/pulls?utf8=%E2%9C%93&q=is%3Aopen+no%3Amilestone+is%3Apr+-label%3A%22pr_action%3A+cleanup%22+-label%3A%22pr_action%3A+merge%22+-label%3A%22pr_action%3A+review%22+-label%3A%22pr_action%3A+discuss%22+-label%3A%22pr_state%3A+blocked%22+-label%3A%22pr_state%3A+WIP%22+).
|
||||
|
||||
# Prioritization of Work
|
||||
|
||||
What should you be working on?
|
||||
|
||||
1. Any PRs that are assigned to you that don't have `pr_state: WIP` or `pr_state: blocked`
|
||||
1. Any issues that are assigned to you in the lowest-numbered Milestone
|
||||
1. Any issues that are assigned to you in any Milestone
|
||||
|
||||
If there are no issues assigned to you in any Milestone, pick an issue, self-assign it, and add
|
||||
it to the most appropriate Milestone based on effort.
|
||||
|
||||
Here are some suggestions for what to work on next:
|
||||
|
||||
* Filter for issues in a component that you are knowledgeable about, and pick something that has a
|
||||
high priority.
|
||||
* Filter for any small effort task that has the special `cust: GT` or `cust:Ionic` tags,
|
||||
and priority > P3.
|
||||
* Add a new task that's really important, add `component`, `priority`, `effort`, `type` and
|
||||
assign it to yourself and the most appropriate milestone.
|
||||
|
||||
# Labels Used in Triage
|
||||
|
||||
## Priority
|
||||
How urgent is this issue? We use priority to determine what should be worked on in each new
|
||||
milestone.
|
||||
|
||||
* `P0: critical` -- drop everything to work on this
|
||||
* `P1: urgent` -- resolve quickly in the current milestone. people are blocked
|
||||
* `P2: required` -- needed for development but not urgent yet. workaround exists, or e.g. new API
|
||||
* `P3: important` -- must complete before Angular 2 is ready for release
|
||||
* `P4: nice to have` -- a good idea, but maybe not until after release
|
||||
|
||||
|
||||
## Effort
|
||||
Rough, non-binding estimate of how much work this issue represents. Please change this assessment
|
||||
for anything you're working on to better reflect reality.
|
||||
|
||||
* `effort: easy` -- straightforward issue that can be resolved in a few hours, e.g. < 1 day of work.
|
||||
* `effort: medium` -- issue that will be a few days of work. Can be completed within a single
|
||||
milestone.
|
||||
* `effort: tough` -- issue that will likely take more than 1 milestone to complete.
|
||||
|
||||
<!-- We don't like these label names as
|
||||
they're not absolute (what is one developer-hour, really?) but decided it wasn't worth arguing
|
||||
over terms. -->
|
||||
|
||||
## Component
|
||||
Which area of Angular knowledge is this issue most closely related to? Helpful when deciding what
|
||||
to work on next.
|
||||
|
||||
* `comp: benchpress` -- benchmarks and performance testing → *tbosch*, *crossj*
|
||||
* `comp: build/dev-productivity` -- build process, e.g. CLI and related tasks → *iminar*, *caitp*
|
||||
* `comp: build/pipeline` -- build pipeline, e.g. ts2dart → *mprobst*, *alexeagle*
|
||||
* `comp: core` -- general core Angular issues, not related to a sub-category (see below) →
|
||||
*mhevery*
|
||||
* `comp: core/animations` -- animations framework → *matsko*
|
||||
* `comp: core/change_detection` -- change detection → *vsavkin*
|
||||
* `comp: core/di` -- dependency injection → *vicb*, *rkirov*
|
||||
* `comp: core/directives` -- directives
|
||||
* `comp: core/forms` -- forms → *vsavkin*
|
||||
* `comp: core/pipes` -- pipes
|
||||
* `comp: core/view` -- runtime processing of the `View`s
|
||||
* `comp: core/view/compiler` -- static analysis of the templates which generate `ProtoView`s.
|
||||
* `comp: core/testbed` -- e2e tests and support for them
|
||||
* `comp: core/webworker` -- core web worker infrastructure
|
||||
* `comp: dart-transformer` -- Dart transforms → *kegluneq*, *jakemac*
|
||||
* `comp: data-access` -- → *jeffbcross*
|
||||
* `comp: docs` -- API docs and doc generation → *naomiblack*, *petebacondarwin*
|
||||
* `comp: material-components` -- Angular Material components built in Angular 2 → *jelbourn*
|
||||
* `comp: router` -- Component Router → *btford*, *igorminar*, *matsko*
|
||||
* `comp: wrenchjs`
|
||||
|
||||
## Type
|
||||
What kind of problem is this?
|
||||
|
||||
* `type RFC / discussion / question`
|
||||
* `type bug`
|
||||
* `type chore`
|
||||
* `type feature`
|
||||
* `type performance`
|
||||
* `type refactor`
|
||||
|
||||
## Special Labels
|
||||
|
||||
### action:design
|
||||
More active discussion is needed before the issue can be worked on further. Typically used for
|
||||
`type: feature` or `type: RFC/discussion/question`
|
||||
|
||||
[See all issues that need discussion](https://github.com/angular/angular/labels/action:%20Design)
|
||||
|
||||
### cla
|
||||
Managed by googlebot. Indicates whether a PR has a CLA on file for its author(s). Only issues with
|
||||
`cla:yes` should be merged into master.
|
||||
|
||||
### cust
|
||||
This is an issue causing user pain for early adopter customers `cust: GT` or `cust: Ionic`.
|
||||
|
||||
### WORKS_AS_INTENDED
|
||||
|
||||
Only used on closed issues, to indicate to the reporter why we closed it.
|
@ -1,73 +0,0 @@
|
||||
{
|
||||
"project": {
|
||||
"version": "1.0.0-beta.32.3",
|
||||
"name": "site"
|
||||
},
|
||||
"apps": [
|
||||
{
|
||||
"root": "src",
|
||||
"outDir": "dist",
|
||||
"assets": [
|
||||
"assets",
|
||||
"content",
|
||||
"app/search/search-worker.js",
|
||||
"favicon.ico"
|
||||
],
|
||||
"index": "index.html",
|
||||
"main": "main.ts",
|
||||
"polyfills": "polyfills.ts",
|
||||
"test": "test.ts",
|
||||
"tsconfig": "tsconfig.json",
|
||||
"prefix": "aio",
|
||||
"serviceWorker": true,
|
||||
"styles": [
|
||||
"styles.scss"
|
||||
],
|
||||
"scripts": [
|
||||
|
||||
],
|
||||
"environmentSource": "environments/environment.ts",
|
||||
"environments": {
|
||||
"dev": "environments/environment.ts",
|
||||
"prod": "environments/environment.prod.ts"
|
||||
}
|
||||
}
|
||||
],
|
||||
"e2e": {
|
||||
"protractor": {
|
||||
"config": "./protractor.conf.js"
|
||||
}
|
||||
},
|
||||
"lint": [
|
||||
{
|
||||
"files": "src/**/*.ts",
|
||||
"project": "src/tsconfig.json"
|
||||
},
|
||||
{
|
||||
"files": "e2e/**/*.ts",
|
||||
"project": "e2e/tsconfig.json"
|
||||
}
|
||||
],
|
||||
"test": {
|
||||
"karma": {
|
||||
"config": "./karma.conf.js"
|
||||
}
|
||||
},
|
||||
"defaults": {
|
||||
"styleExt": "scss",
|
||||
"component": {},
|
||||
"prefixInterfaces": false,
|
||||
"inline": {
|
||||
"style": false,
|
||||
"template": false
|
||||
},
|
||||
"spec": {
|
||||
"class": false,
|
||||
"component": true,
|
||||
"directive": true,
|
||||
"module": false,
|
||||
"pipe": true,
|
||||
"service": true
|
||||
}
|
||||
}
|
||||
}
|
@ -1,5 +0,0 @@
|
||||
{
|
||||
"projects": {
|
||||
"staging": "aio-staging"
|
||||
}
|
||||
}
|
11
aio/.gitignore
vendored
11
aio/.gitignore
vendored
@ -1,11 +0,0 @@
|
||||
# Ignore node_modules
|
||||
node_modules
|
||||
|
||||
# Ignore npm/yarn debug log
|
||||
npm-debug.log
|
||||
yarn-error.log
|
||||
|
||||
# Ignore generated content
|
||||
/dist
|
||||
/src/content
|
||||
/.sass-cache
|
@ -1,31 +0,0 @@
|
||||
# Site
|
||||
|
||||
This project was generated with [angular-cli](https://github.com/angular/angular-cli) version 1.0.0-beta.26.
|
||||
|
||||
## Development server
|
||||
Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files.
|
||||
|
||||
## Code scaffolding
|
||||
|
||||
Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive/pipe/service/class/module`.
|
||||
|
||||
## Build
|
||||
|
||||
Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `--prod` flag for a production build.
|
||||
|
||||
## Running unit tests
|
||||
|
||||
Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io).
|
||||
|
||||
## Running end-to-end tests
|
||||
|
||||
Run `ng e2e` to execute the end-to-end tests via [Protractor](http://www.protractortest.org/).
|
||||
Before running the tests make sure you are serving the app via `ng serve`.
|
||||
|
||||
## Deploying to GitHub Pages
|
||||
|
||||
Run `ng github-pages:deploy` to deploy to GitHub Pages.
|
||||
|
||||
## Further help
|
||||
|
||||
To get more help on the `angular-cli` use `ng help` or go check out the [Angular-CLI README](https://github.com/angular/angular-cli/blob/master/README.md).
|
@ -1,3 +0,0 @@
|
||||
scripts-js/lib
|
||||
scripts-js/node_modules
|
||||
scripts-js/**/test
|
@ -1,158 +0,0 @@
|
||||
# Image metadata and config
|
||||
FROM debian:jessie
|
||||
|
||||
LABEL name="angular.io PR preview" \
|
||||
description="This image implements the PR preview functionality for angular.io." \
|
||||
vendor="Angular" \
|
||||
version="1.0"
|
||||
|
||||
VOLUME /aio-secrets
|
||||
VOLUME /var/www/aio-builds
|
||||
|
||||
EXPOSE 80 443
|
||||
|
||||
|
||||
# Build-time args and env vars
|
||||
ARG AIO_BUILDS_DIR=/var/www/aio-builds
|
||||
ARG TEST_AIO_BUILDS_DIR=/tmp/aio-builds
|
||||
ARG AIO_DOMAIN_NAME=ngbuilds.io
|
||||
ARG TEST_AIO_DOMAIN_NAME=$AIO_DOMAIN_NAME.localhost
|
||||
ARG AIO_GITHUB_ORGANIZATION=angular
|
||||
ARG TEST_AIO_GITHUB_ORGANIZATION=angular
|
||||
ARG AIO_GITHUB_TEAM_SLUGS=angular-core,aio-contributors
|
||||
ARG TEST_AIO_GITHUB_TEAM_SLUGS=angular-core,aio-contributors
|
||||
ARG AIO_NGINX_HOSTNAME=$AIO_DOMAIN_NAME
|
||||
ARG TEST_AIO_NGINX_HOSTNAME=$TEST_AIO_DOMAIN_NAME
|
||||
ARG AIO_NGINX_PORT_HTTP=80
|
||||
ARG TEST_AIO_NGINX_PORT_HTTP=8080
|
||||
ARG AIO_NGINX_PORT_HTTPS=443
|
||||
ARG TEST_AIO_NGINX_PORT_HTTPS=4433
|
||||
ARG AIO_REPO_SLUG=angular/angular
|
||||
ARG TEST_AIO_REPO_SLUG=test-repo/test-slug
|
||||
ARG AIO_UPLOAD_HOSTNAME=upload.localhost
|
||||
ARG TEST_AIO_UPLOAD_HOSTNAME=upload.localhost
|
||||
ARG AIO_UPLOAD_MAX_SIZE=20971520
|
||||
ARG TEST_AIO_UPLOAD_MAX_SIZE=20971520
|
||||
ARG AIO_UPLOAD_PORT=3000
|
||||
ARG TEST_AIO_UPLOAD_PORT=3001
|
||||
|
||||
ENV AIO_BUILDS_DIR=$AIO_BUILDS_DIR TEST_AIO_BUILDS_DIR=$TEST_AIO_BUILDS_DIR \
|
||||
AIO_DOMAIN_NAME=$AIO_DOMAIN_NAME TEST_AIO_DOMAIN_NAME=$TEST_AIO_DOMAIN_NAME \
|
||||
AIO_GITHUB_ORGANIZATION=$AIO_GITHUB_ORGANIZATION TEST_AIO_GITHUB_ORGANIZATION=$TEST_AIO_GITHUB_ORGANIZATION \
|
||||
AIO_GITHUB_TEAM_SLUGS=$AIO_GITHUB_TEAM_SLUGS TEST_AIO_GITHUB_TEAM_SLUGS=$TEST_AIO_GITHUB_TEAM_SLUGS \
|
||||
AIO_LOCALCERTS_DIR=/etc/ssl/localcerts TEST_AIO_LOCALCERTS_DIR=/etc/ssl/localcerts-test \
|
||||
AIO_NGINX_HOSTNAME=$AIO_NGINX_HOSTNAME TEST_AIO_NGINX_HOSTNAME=$TEST_AIO_NGINX_HOSTNAME \
|
||||
AIO_NGINX_LOGS_DIR=/var/log/aio/nginx TEST_AIO_NGINX_LOGS_DIR=/var/log/aio/nginx-test \
|
||||
AIO_NGINX_PORT_HTTP=$AIO_NGINX_PORT_HTTP TEST_AIO_NGINX_PORT_HTTP=$TEST_AIO_NGINX_PORT_HTTP \
|
||||
AIO_NGINX_PORT_HTTPS=$AIO_NGINX_PORT_HTTPS TEST_AIO_NGINX_PORT_HTTPS=$TEST_AIO_NGINX_PORT_HTTPS \
|
||||
AIO_REPO_SLUG=$AIO_REPO_SLUG TEST_AIO_REPO_SLUG=$TEST_AIO_REPO_SLUG \
|
||||
AIO_SCRIPTS_JS_DIR=/usr/share/aio-scripts-js \
|
||||
AIO_SCRIPTS_SH_DIR=/usr/share/aio-scripts-sh \
|
||||
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_PORT=$AIO_UPLOAD_PORT TEST_AIO_UPLOAD_PORT=$TEST_AIO_UPLOAD_PORT \
|
||||
NODE_ENV=production
|
||||
|
||||
|
||||
# Create directory for logs
|
||||
RUN mkdir /var/log/aio
|
||||
|
||||
|
||||
# Add extra package sources
|
||||
RUN apt-get update -y && apt-get install -y curl
|
||||
RUN curl --silent --show-error --location https://deb.nodesource.com/setup_6.x | bash -
|
||||
RUN curl --silent --show-error https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add -
|
||||
RUN echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list
|
||||
|
||||
|
||||
# Install packages
|
||||
RUN apt-get update -y && apt-get install -y \
|
||||
chkconfig \
|
||||
cron \
|
||||
dnsmasq \
|
||||
nano \
|
||||
nginx \
|
||||
nodejs \
|
||||
openssl \
|
||||
rsyslog \
|
||||
yarn
|
||||
RUN yarn global add pm2@2
|
||||
|
||||
|
||||
# Set up cronjobs
|
||||
COPY cronjobs/aio-builds-cleanup /etc/cron.d/
|
||||
RUN chmod 0744 /etc/cron.d/aio-builds-cleanup
|
||||
RUN crontab /etc/cron.d/aio-builds-cleanup
|
||||
RUN printenv | grep AIO_ >> /etc/environment
|
||||
|
||||
|
||||
# Set up dnsmasq
|
||||
COPY dnsmasq/dnsmasq.conf /etc/
|
||||
RUN sed -i "s|{{\$AIO_NGINX_HOSTNAME}}|$AIO_NGINX_HOSTNAME|g" /etc/dnsmasq.conf
|
||||
RUN sed -i "s|{{\$AIO_UPLOAD_HOSTNAME}}|$AIO_UPLOAD_HOSTNAME|g" /etc/dnsmasq.conf
|
||||
RUN sed -i "s|{{\$TEST_AIO_NGINX_HOSTNAME}}|$TEST_AIO_NGINX_HOSTNAME|g" /etc/dnsmasq.conf
|
||||
RUN sed -i "s|{{\$TEST_AIO_UPLOAD_HOSTNAME}}|$TEST_AIO_UPLOAD_HOSTNAME|g" /etc/dnsmasq.conf
|
||||
|
||||
|
||||
# Set up SSL/TLS certificates
|
||||
COPY nginx/create-selfsigned-cert.sh /tmp/
|
||||
RUN chmod a+x /tmp/create-selfsigned-cert.sh
|
||||
RUN /tmp/create-selfsigned-cert.sh "selfcert-prod" "$AIO_NGINX_HOSTNAME" "$AIO_LOCALCERTS_DIR"
|
||||
RUN /tmp/create-selfsigned-cert.sh "selfcert-test" "$TEST_AIO_NGINX_HOSTNAME" "$TEST_AIO_LOCALCERTS_DIR"
|
||||
RUN rm /tmp/create-selfsigned-cert.sh
|
||||
RUN update-ca-certificates
|
||||
|
||||
|
||||
# Set up nginx (for production and testing)
|
||||
RUN rm /etc/nginx/sites-enabled/*
|
||||
|
||||
COPY nginx/aio-builds.conf /etc/nginx/sites-available/aio-builds-prod.conf
|
||||
RUN sed -i "s|{{\$AIO_BUILDS_DIR}}|$AIO_BUILDS_DIR|g" /etc/nginx/sites-available/aio-builds-prod.conf
|
||||
RUN sed -i "s|{{\$AIO_DOMAIN_NAME}}|$AIO_DOMAIN_NAME|g" /etc/nginx/sites-available/aio-builds-prod.conf
|
||||
RUN sed -i "s|{{\$AIO_LOCALCERTS_DIR}}|$AIO_LOCALCERTS_DIR|g" /etc/nginx/sites-available/aio-builds-prod.conf
|
||||
RUN sed -i "s|{{\$AIO_NGINX_LOGS_DIR}}|$AIO_NGINX_LOGS_DIR|g" /etc/nginx/sites-available/aio-builds-prod.conf
|
||||
RUN sed -i "s|{{\$AIO_NGINX_PORT_HTTP}}|$AIO_NGINX_PORT_HTTP|g" /etc/nginx/sites-available/aio-builds-prod.conf
|
||||
RUN sed -i "s|{{\$AIO_NGINX_PORT_HTTPS}}|$AIO_NGINX_PORT_HTTPS|g" /etc/nginx/sites-available/aio-builds-prod.conf
|
||||
RUN sed -i "s|{{\$AIO_UPLOAD_HOSTNAME}}|$AIO_UPLOAD_HOSTNAME|g" /etc/nginx/sites-available/aio-builds-prod.conf
|
||||
RUN sed -i "s|{{\$AIO_UPLOAD_MAX_SIZE}}|$AIO_UPLOAD_MAX_SIZE|g" /etc/nginx/sites-available/aio-builds-prod.conf
|
||||
RUN sed -i "s|{{\$AIO_UPLOAD_PORT}}|$AIO_UPLOAD_PORT|g" /etc/nginx/sites-available/aio-builds-prod.conf
|
||||
RUN ln -s /etc/nginx/sites-available/aio-builds-prod.conf /etc/nginx/sites-enabled/aio-builds-prod.conf
|
||||
|
||||
COPY nginx/aio-builds.conf /etc/nginx/sites-available/aio-builds-test.conf
|
||||
RUN sed -i "s|{{\$AIO_BUILDS_DIR}}|$TEST_AIO_BUILDS_DIR|g" /etc/nginx/sites-available/aio-builds-test.conf
|
||||
RUN sed -i "s|{{\$AIO_DOMAIN_NAME}}|$TEST_AIO_DOMAIN_NAME|g" /etc/nginx/sites-available/aio-builds-test.conf
|
||||
RUN sed -i "s|{{\$AIO_LOCALCERTS_DIR}}|$TEST_AIO_LOCALCERTS_DIR|g" /etc/nginx/sites-available/aio-builds-test.conf
|
||||
RUN sed -i "s|{{\$AIO_NGINX_LOGS_DIR}}|$TEST_AIO_NGINX_LOGS_DIR|g" /etc/nginx/sites-available/aio-builds-test.conf
|
||||
RUN sed -i "s|{{\$AIO_NGINX_PORT_HTTP}}|$TEST_AIO_NGINX_PORT_HTTP|g" /etc/nginx/sites-available/aio-builds-test.conf
|
||||
RUN sed -i "s|{{\$AIO_NGINX_PORT_HTTPS}}|$TEST_AIO_NGINX_PORT_HTTPS|g" /etc/nginx/sites-available/aio-builds-test.conf
|
||||
RUN sed -i "s|{{\$AIO_UPLOAD_HOSTNAME}}|$TEST_AIO_UPLOAD_HOSTNAME|g" /etc/nginx/sites-available/aio-builds-test.conf
|
||||
RUN sed -i "s|{{\$AIO_UPLOAD_MAX_SIZE}}|$TEST_AIO_UPLOAD_MAX_SIZE|g" /etc/nginx/sites-available/aio-builds-test.conf
|
||||
RUN sed -i "s|{{\$AIO_UPLOAD_PORT}}|$TEST_AIO_UPLOAD_PORT|g" /etc/nginx/sites-available/aio-builds-test.conf
|
||||
RUN ln -s /etc/nginx/sites-available/aio-builds-test.conf /etc/nginx/sites-enabled/aio-builds-test.conf
|
||||
|
||||
|
||||
# Set up pm2
|
||||
RUN pm2 startup systemv -u root > /dev/null
|
||||
RUN chkconfig pm2-root on
|
||||
|
||||
|
||||
# Set up the shell scripts
|
||||
COPY scripts-sh/ $AIO_SCRIPTS_SH_DIR/
|
||||
RUN chmod a+x $AIO_SCRIPTS_SH_DIR/*
|
||||
RUN find $AIO_SCRIPTS_SH_DIR -maxdepth 1 -type f -printf "%P\n" \
|
||||
| while read file; do ln -s $AIO_SCRIPTS_SH_DIR/$file /usr/local/bin/aio-${file%.*}; done
|
||||
|
||||
|
||||
# Set up the Node.js scripts
|
||||
COPY scripts-js/ $AIO_SCRIPTS_JS_DIR/
|
||||
WORKDIR $AIO_SCRIPTS_JS_DIR/
|
||||
RUN yarn install --production
|
||||
|
||||
|
||||
# Set up health check
|
||||
HEALTHCHECK --interval=5m CMD /usr/local/bin/aio-health-check
|
||||
|
||||
|
||||
# Go!
|
||||
WORKDIR /
|
||||
CMD aio-init && tail -f /dev/null
|
@ -1,2 +0,0 @@
|
||||
# Periodically clean up builds that do not correspond to currently open PRs
|
||||
0 12 * * * root /usr/local/bin/aio-clean-up >> /var/log/cron.log 2>&1
|
@ -1,16 +0,0 @@
|
||||
# Do not read /etc/resolv.conf. Get servers from this file instead.
|
||||
no-resolv
|
||||
server=8.8.8.8
|
||||
server=8.8.4.4
|
||||
|
||||
# Listen for DHCP and DNS requests only on this address.
|
||||
listen-address=127.0.0.1
|
||||
|
||||
# Force an IP addres for these domains.
|
||||
address=/{{$AIO_NGINX_HOSTNAME}}/127.0.0.1
|
||||
address=/{{$AIO_UPLOAD_HOSTNAME}}/127.0.0.1
|
||||
address=/{{$TEST_AIO_NGINX_HOSTNAME}}/127.0.0.1
|
||||
address=/{{$TEST_AIO_UPLOAD_HOSTNAME}}/127.0.0.1
|
||||
|
||||
# Run as root (required from inside docker container).
|
||||
user=root
|
@ -1,87 +0,0 @@
|
||||
# Redirect all HTTP traffic to HTTPS
|
||||
server {
|
||||
server_name _;
|
||||
|
||||
listen {{$AIO_NGINX_PORT_HTTP}} default_server;
|
||||
listen [::]:{{$AIO_NGINX_PORT_HTTP}};
|
||||
|
||||
access_log {{$AIO_NGINX_LOGS_DIR}}/access.log;
|
||||
error_log {{$AIO_NGINX_LOGS_DIR}}/error.log;
|
||||
|
||||
# Ideally we want 308 (permanent + keep original method),
|
||||
# but it is relatively new and not supported by some clients (e.g. cURL).
|
||||
return 307 https://$host:{{$AIO_NGINX_PORT_HTTPS}}$request_uri;
|
||||
}
|
||||
|
||||
# Serve PR-preview requests
|
||||
server {
|
||||
server_name "~^pr(?<pr>[1-9][0-9]*)-(?<sha>[0-9a-f]{40})\.";
|
||||
|
||||
listen {{$AIO_NGINX_PORT_HTTPS}} ssl;
|
||||
listen [::]:{{$AIO_NGINX_PORT_HTTPS}} ssl;
|
||||
|
||||
ssl_certificate {{$AIO_LOCALCERTS_DIR}}/{{$AIO_DOMAIN_NAME}}.crt;
|
||||
ssl_certificate_key {{$AIO_LOCALCERTS_DIR}}/{{$AIO_DOMAIN_NAME}}.key;
|
||||
|
||||
root {{$AIO_BUILDS_DIR}}/$pr/$sha;
|
||||
disable_symlinks on from=$document_root;
|
||||
index index.html;
|
||||
|
||||
access_log {{$AIO_NGINX_LOGS_DIR}}/access.log;
|
||||
error_log {{$AIO_NGINX_LOGS_DIR}}/error.log;
|
||||
|
||||
location "~/[^/]+\.[^/]+$" {
|
||||
try_files $uri $uri/ =404;
|
||||
}
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html =404;
|
||||
}
|
||||
}
|
||||
|
||||
# Handle all other requests
|
||||
server {
|
||||
server_name _;
|
||||
|
||||
listen {{$AIO_NGINX_PORT_HTTPS}} ssl default_server;
|
||||
listen [::]:{{$AIO_NGINX_PORT_HTTPS}} ssl;
|
||||
|
||||
ssl_certificate {{$AIO_LOCALCERTS_DIR}}/{{$AIO_DOMAIN_NAME}}.crt;
|
||||
ssl_certificate_key {{$AIO_LOCALCERTS_DIR}}/{{$AIO_DOMAIN_NAME}}.key;
|
||||
|
||||
access_log {{$AIO_NGINX_LOGS_DIR}}/access.log;
|
||||
error_log {{$AIO_NGINX_LOGS_DIR}}/error.log;
|
||||
|
||||
# Health check
|
||||
location "~^/health-check/?$" {
|
||||
add_header Content-Type text/plain;
|
||||
return 200 '';
|
||||
}
|
||||
|
||||
# Upload builds
|
||||
location "~^/create-build/(?<pr>[1-9][0-9]*)/(?<sha>[0-9a-f]{40})/?$" {
|
||||
if ($request_method != "POST") {
|
||||
add_header Allow "POST";
|
||||
return 405;
|
||||
}
|
||||
|
||||
client_body_temp_path /tmp/aio-create-builds;
|
||||
client_body_buffer_size 128K;
|
||||
client_max_body_size {{$AIO_UPLOAD_MAX_SIZE}};
|
||||
client_body_in_file_only on;
|
||||
|
||||
proxy_pass_request_headers on;
|
||||
proxy_set_header X-FILE $request_body_file;
|
||||
proxy_set_body off;
|
||||
proxy_redirect off;
|
||||
proxy_method GET;
|
||||
proxy_pass http://{{$AIO_UPLOAD_HOSTNAME}}:{{$AIO_UPLOAD_PORT}}$request_uri;
|
||||
|
||||
resolver 127.0.0.1;
|
||||
}
|
||||
|
||||
# Everything else
|
||||
location / {
|
||||
return 404;
|
||||
}
|
||||
}
|
@ -1,20 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -eu -o pipefail
|
||||
|
||||
|
||||
# Variables
|
||||
confFile=/tmp/$1.conf
|
||||
domainName=$2
|
||||
outDir=$3
|
||||
|
||||
|
||||
# Create certificate
|
||||
cp /etc/ssl/openssl.cnf "$confFile"
|
||||
echo "[subjectAltName]" >> "$confFile"
|
||||
echo "subjectAltName = DNS:$domainName, DNS:*.$domainName" >> "$confFile"
|
||||
mkdir -p $outDir
|
||||
openssl req -days 365 -newkey rsa:2048 -nodes -sha256 -x509 \
|
||||
-config "$confFile" -extensions subjectAltName -subj "/CN=$domainName" \
|
||||
-out "$outDir/$domainName.crt" -keyout "$outDir/$domainName.key"
|
||||
chmod -R 400 "$outDir"
|
||||
cp "$outDir/$domainName.crt" /usr/local/share/ca-certificates
|
@ -1 +0,0 @@
|
||||
/dist/
|
@ -1,71 +0,0 @@
|
||||
// Imports
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as shell from 'shelljs';
|
||||
import {GithubPullRequests} from '../common/github-pull-requests';
|
||||
import {assertNotMissingOrEmpty} from '../common/utils';
|
||||
|
||||
// Classes
|
||||
export class BuildCleaner {
|
||||
// Constructor
|
||||
constructor(protected buildsDir: string, protected repoSlug: string, protected githubToken: string) {
|
||||
assertNotMissingOrEmpty('buildsDir', buildsDir);
|
||||
assertNotMissingOrEmpty('repoSlug', repoSlug);
|
||||
assertNotMissingOrEmpty('githubToken', githubToken);
|
||||
}
|
||||
|
||||
// Methods - Public
|
||||
public cleanUp(): Promise<void> {
|
||||
return Promise.all([
|
||||
this.getExistingBuildNumbers(),
|
||||
this.getOpenPrNumbers(),
|
||||
]).then(([existingBuilds, openPrs]) => this.removeUnnecessaryBuilds(existingBuilds, openPrs));
|
||||
}
|
||||
|
||||
// Methods - Protected
|
||||
protected getExistingBuildNumbers(): Promise<number[]> {
|
||||
return new Promise((resolve, reject) => {
|
||||
fs.readdir(this.buildsDir, (err, files) => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
|
||||
const buildNumbers = files.
|
||||
map(Number). // Convert string to number
|
||||
filter(Boolean); // Ignore NaN (or 0), because they are not builds
|
||||
|
||||
resolve(buildNumbers);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
protected getOpenPrNumbers(): Promise<number[]> {
|
||||
const githubPullRequests = new GithubPullRequests(this.githubToken, this.repoSlug);
|
||||
|
||||
return githubPullRequests.
|
||||
fetchAll('open').
|
||||
then(prs => prs.map(pr => pr.number));
|
||||
}
|
||||
|
||||
protected removeDir(dir: string) {
|
||||
try {
|
||||
// Undocumented signature (see https://github.com/shelljs/shelljs/pull/663).
|
||||
(shell as any).chmod('-R', 'a+w', dir);
|
||||
shell.rm('-rf', dir);
|
||||
} catch (err) {
|
||||
console.error(`ERROR: Unable to remove '${dir}' due to:`, err);
|
||||
}
|
||||
}
|
||||
|
||||
protected removeUnnecessaryBuilds(existingBuildNumbers: number[], openPrNumbers: number[]) {
|
||||
const toRemove = existingBuildNumbers.filter(num => !openPrNumbers.includes(num));
|
||||
|
||||
console.log(`Existing builds: ${existingBuildNumbers.length}`);
|
||||
console.log(`Open pull requests: ${openPrNumbers.length}`);
|
||||
console.log(`Removing ${toRemove.length} build(s): ${toRemove.join(', ')}`);
|
||||
|
||||
toRemove.
|
||||
map(num => path.join(this.buildsDir, String(num))).
|
||||
forEach(dir => this.removeDir(dir));
|
||||
}
|
||||
}
|
@ -1,23 +0,0 @@
|
||||
// Imports
|
||||
import {getEnvVar} from '../common/utils';
|
||||
import {BuildCleaner} from './build-cleaner';
|
||||
|
||||
// Constants
|
||||
const AIO_BUILDS_DIR = getEnvVar('AIO_BUILDS_DIR');
|
||||
const AIO_GITHUB_TOKEN = getEnvVar('AIO_GITHUB_TOKEN', true);
|
||||
const AIO_REPO_SLUG = getEnvVar('AIO_REPO_SLUG');
|
||||
|
||||
// Run
|
||||
_main();
|
||||
|
||||
// Functions
|
||||
function _main() {
|
||||
console.log(`[${new Date()}] - Cleaning up builds...`);
|
||||
|
||||
const buildCleaner = new BuildCleaner(AIO_BUILDS_DIR, AIO_REPO_SLUG, AIO_GITHUB_TOKEN);
|
||||
|
||||
buildCleaner.cleanUp().catch(err => {
|
||||
console.error('ERROR:', err);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
@ -1,110 +0,0 @@
|
||||
// Imports
|
||||
import {IncomingMessage} from 'http';
|
||||
import * as https from 'https';
|
||||
import {assertNotMissingOrEmpty} from './utils';
|
||||
|
||||
// Constants
|
||||
const GITHUB_HOSTNAME = 'api.github.com';
|
||||
|
||||
// Interfaces - Types
|
||||
interface RequestParams {
|
||||
[key: string]: string | number;
|
||||
}
|
||||
|
||||
type RequestParamsOrNull = RequestParams | null;
|
||||
|
||||
// Classes
|
||||
export class GithubApi {
|
||||
protected requestHeaders: {[key: string]: string};
|
||||
|
||||
// Constructor
|
||||
constructor(githubToken: string) {
|
||||
assertNotMissingOrEmpty('githubToken', githubToken);
|
||||
|
||||
this.requestHeaders = {
|
||||
'Authorization': `token ${githubToken}`,
|
||||
'User-Agent': `Node/${process.versions.node}`,
|
||||
};
|
||||
}
|
||||
|
||||
// Methods - Public
|
||||
public get<T>(pathname: string, params?: RequestParamsOrNull): Promise<T> {
|
||||
const path = this.buildPath(pathname, params);
|
||||
return this.request<T>('get', path);
|
||||
}
|
||||
|
||||
public post<T>(pathname: string, params?: RequestParamsOrNull, data?: any): Promise<T> {
|
||||
const path = this.buildPath(pathname, params);
|
||||
return this.request<T>('post', path, data);
|
||||
}
|
||||
|
||||
// Methods - Protected
|
||||
protected buildPath(pathname: string, params?: RequestParamsOrNull): string {
|
||||
if (params == null) {
|
||||
return pathname;
|
||||
}
|
||||
|
||||
const search = (params === null) ? '' : this.serializeSearchParams(params);
|
||||
const joiner = search && '?';
|
||||
|
||||
return `${pathname}${joiner}${search}`;
|
||||
}
|
||||
|
||||
protected getPaginated<T>(pathname: string, baseParams: RequestParams = {}, currentPage: number = 0): Promise<T[]> {
|
||||
const perPage = 100;
|
||||
const params = {
|
||||
...baseParams,
|
||||
page: currentPage,
|
||||
per_page: perPage,
|
||||
};
|
||||
|
||||
return this.get<T[]>(pathname, params).then(items => {
|
||||
if (items.length < perPage) {
|
||||
return items;
|
||||
}
|
||||
|
||||
return this.getPaginated(pathname, baseParams, currentPage + 1).then(moreItems => [...items, ...moreItems]);
|
||||
});
|
||||
}
|
||||
|
||||
protected request<T>(method: string, path: string, data: any = null): Promise<T> {
|
||||
return new Promise<T>((resolve, reject) => {
|
||||
const options = {
|
||||
headers: {...this.requestHeaders},
|
||||
host: GITHUB_HOSTNAME,
|
||||
method,
|
||||
path,
|
||||
};
|
||||
|
||||
const onError = (statusCode: number, responseText: string) => {
|
||||
const url = `https://${GITHUB_HOSTNAME}${path}`;
|
||||
reject(`Request to '${url}' failed (status: ${statusCode}): ${responseText}`);
|
||||
};
|
||||
const onSuccess = (responseText: string) => {
|
||||
try { resolve(JSON.parse(responseText)); } catch (err) { reject(err); }
|
||||
};
|
||||
const onResponse = (res: IncomingMessage) => {
|
||||
const statusCode = res.statusCode || -1;
|
||||
const isSuccess = (200 <= statusCode) && (statusCode < 400);
|
||||
let responseText = '';
|
||||
|
||||
res.
|
||||
on('data', d => responseText += d).
|
||||
on('end', () => isSuccess ? onSuccess(responseText) : onError(statusCode, responseText)).
|
||||
on('error', reject);
|
||||
};
|
||||
|
||||
https.
|
||||
request(options, onResponse).
|
||||
on('error', reject).
|
||||
end(data && JSON.stringify(data));
|
||||
});
|
||||
}
|
||||
|
||||
protected serializeSearchParams(params: RequestParams): string {
|
||||
return Object.keys(params).
|
||||
filter(key => params[key] != null).
|
||||
map(key => `${key}=${encodeURIComponent(String(params[key]))}`).
|
||||
join('&');
|
||||
}
|
||||
}
|
@ -1,44 +0,0 @@
|
||||
// Imports
|
||||
import {assertNotMissingOrEmpty} from '../common/utils';
|
||||
import {GithubApi} from './github-api';
|
||||
|
||||
// Interfaces - Types
|
||||
export interface PullRequest {
|
||||
number: number;
|
||||
user: {login: string};
|
||||
}
|
||||
|
||||
export type PullRequestState = 'all' | 'closed' | 'open';
|
||||
|
||||
// Classes
|
||||
export class GithubPullRequests extends GithubApi {
|
||||
// Constructor
|
||||
constructor(githubToken: string, protected repoSlug: string) {
|
||||
super(githubToken);
|
||||
assertNotMissingOrEmpty('repoSlug', repoSlug);
|
||||
}
|
||||
|
||||
// Methods - Public
|
||||
public addComment(pr: number, body: string): Promise<void> {
|
||||
if (!(pr > 0)) {
|
||||
throw new Error(`Invalid PR number: ${pr}`);
|
||||
} else if (!body) {
|
||||
throw new Error(`Invalid or empty comment body: ${body}`);
|
||||
}
|
||||
|
||||
return this.post<void>(`/repos/${this.repoSlug}/issues/${pr}/comments`, null, {body});
|
||||
}
|
||||
|
||||
public fetch(pr: number): Promise<PullRequest> {
|
||||
return this.get<PullRequest>(`/repos/${this.repoSlug}/pulls/${pr}`);
|
||||
}
|
||||
|
||||
public fetchAll(state: PullRequestState = 'all'): Promise<PullRequest[]> {
|
||||
console.log(`Fetching ${state} pull requests...`);
|
||||
|
||||
const pathname = `/repos/${this.repoSlug}/pulls`;
|
||||
const params = {state};
|
||||
|
||||
return this.getPaginated<PullRequest>(pathname, params);
|
||||
}
|
||||
}
|
@ -1,45 +0,0 @@
|
||||
// Imports
|
||||
import {assertNotMissingOrEmpty} from '../common/utils';
|
||||
import {GithubApi} from './github-api';
|
||||
|
||||
// Interfaces - Types
|
||||
interface Team {
|
||||
id: number;
|
||||
slug: string;
|
||||
}
|
||||
|
||||
interface TeamMembership {
|
||||
state: string;
|
||||
}
|
||||
|
||||
// Classes
|
||||
export class GithubTeams extends GithubApi {
|
||||
// Constructor
|
||||
constructor(githubToken: string, protected organization: string) {
|
||||
super(githubToken);
|
||||
assertNotMissingOrEmpty('organization', organization);
|
||||
}
|
||||
|
||||
// Methods - Public
|
||||
public fetchAll(): Promise<Team[]> {
|
||||
return this.getPaginated<Team>(`/orgs/${this.organization}/teams`);
|
||||
}
|
||||
|
||||
public isMemberById(username: string, teamIds: number[]): Promise<boolean> {
|
||||
const getMembership = (teamId: number) =>
|
||||
this.get<TeamMembership>(`/teams/${teamId}/memberships/${username}`).
|
||||
then(membership => membership.state === 'active').
|
||||
catch(() => false);
|
||||
const reduceFn = (promise: Promise<boolean>, teamId: number) =>
|
||||
promise.then(isMember => isMember || getMembership(teamId));
|
||||
|
||||
return teamIds.reduce(reduceFn, Promise.resolve(false));
|
||||
}
|
||||
|
||||
public isMemberBySlug(username: string, teamSlugs: string[]): Promise<boolean> {
|
||||
return this.fetchAll().
|
||||
then(teams => teams.filter(team => teamSlugs.includes(team.slug)).map(team => team.id)).
|
||||
then(teamIds => this.isMemberById(username, teamIds)).
|
||||
catch(() => false);
|
||||
}
|
||||
}
|
@ -1,23 +0,0 @@
|
||||
export const runTests = (specFiles: string[], helpers?: string[]) => {
|
||||
// We can't use `import` here, because of the following mess:
|
||||
// - GitHub project `jasmine/jasmine` is `jasmine-core` on npm and its typings `@types/jasmine`.
|
||||
// - GitHub project `jasmine/jasmine-npm` is `jasmine` on npm and has no typings.
|
||||
//
|
||||
// Using `import...from 'jasmine'` here, would import from `@types/jasmine` (which refers to the
|
||||
// `jasmine-core` module and the `jasmine` module).
|
||||
// tslint:disable-next-line: no-var-requires variable-name
|
||||
const Jasmine = require('jasmine');
|
||||
const config = {
|
||||
helpers,
|
||||
random: true,
|
||||
spec_files: specFiles,
|
||||
stopSpecOnExpectationFailure: true,
|
||||
};
|
||||
|
||||
process.on('unhandledRejection', (reason: any) => console.log('Unhandled rejection:', reason));
|
||||
|
||||
const runner = new Jasmine();
|
||||
runner.loadConfig(config);
|
||||
runner.onComplete((passed: boolean) => process.exit(passed ? 0 : 1));
|
||||
runner.execute();
|
||||
};
|
@ -1,17 +0,0 @@
|
||||
// Functions
|
||||
export const assertNotMissingOrEmpty = (name: string, value: string | null | undefined) => {
|
||||
if (!value) {
|
||||
throw new Error(`Missing or empty required parameter '${name}'!`);
|
||||
}
|
||||
};
|
||||
|
||||
export const getEnvVar = (name: string, isOptional = false): string => {
|
||||
const value = process.env[name];
|
||||
|
||||
if (!isOptional && !value) {
|
||||
console.error(`ERROR: Missing required environment variable '${name}'!`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
return value || '';
|
||||
};
|
@ -1,81 +0,0 @@
|
||||
// Imports
|
||||
import * as cp from 'child_process';
|
||||
import {EventEmitter} from 'events';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as shell from 'shelljs';
|
||||
import {assertNotMissingOrEmpty} from '../common/utils';
|
||||
import {CreatedBuildEvent} from './build-events';
|
||||
import {UploadError} from './upload-error';
|
||||
|
||||
// Classes
|
||||
export class BuildCreator extends EventEmitter {
|
||||
// Constructor
|
||||
constructor(protected buildsDir: string) {
|
||||
super();
|
||||
assertNotMissingOrEmpty('buildsDir', buildsDir);
|
||||
}
|
||||
|
||||
// Methods - Public
|
||||
public create(pr: string, sha: string, archivePath: string): Promise<any> {
|
||||
const prDir = path.join(this.buildsDir, pr);
|
||||
const shaDir = path.join(prDir, sha);
|
||||
let dirToRemoveOnError: string;
|
||||
|
||||
return Promise.
|
||||
all([this.exists(prDir), this.exists(shaDir)]).
|
||||
then(([prDirExisted, shaDirExisted]) => {
|
||||
if (shaDirExisted) {
|
||||
throw new UploadError(403, `Request to overwrite existing directory: ${shaDir}`);
|
||||
}
|
||||
|
||||
dirToRemoveOnError = prDirExisted ? shaDir : prDir;
|
||||
|
||||
return Promise.resolve().
|
||||
then(() => shell.mkdir('-p', shaDir)).
|
||||
then(() => this.extractArchive(archivePath, shaDir)).
|
||||
then(() => this.emit(CreatedBuildEvent.type, new CreatedBuildEvent(+pr, sha)));
|
||||
}).
|
||||
catch(err => {
|
||||
if (dirToRemoveOnError) {
|
||||
shell.rm('-rf', dirToRemoveOnError);
|
||||
}
|
||||
|
||||
if (!(err instanceof UploadError)) {
|
||||
err = new UploadError(500, `Error while uploading to directory: ${shaDir}\n${err}`);
|
||||
}
|
||||
|
||||
throw err;
|
||||
});
|
||||
}
|
||||
|
||||
// Methods - Protected
|
||||
protected exists(fileOrDir: string): Promise<boolean> {
|
||||
return new Promise(resolve => fs.access(fileOrDir, err => resolve(!err)));
|
||||
}
|
||||
|
||||
protected extractArchive(inputFile: string, outputDir: string): Promise<void> {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
const cmd = `tar --extract --gzip --directory "${outputDir}" --file "${inputFile}"`;
|
||||
|
||||
cp.exec(cmd, (err, _stdout, stderr) => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
|
||||
if (stderr) {
|
||||
console.warn(stderr);
|
||||
}
|
||||
|
||||
try {
|
||||
// Undocumented signature (see https://github.com/shelljs/shelljs/pull/663).
|
||||
(shell as any).chmod('-R', 'a-w', outputDir);
|
||||
shell.rm('-f', inputFile);
|
||||
resolve();
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
@ -1,15 +0,0 @@
|
||||
// Classes
|
||||
export class BuildEvent {
|
||||
// Constructor
|
||||
constructor(public type: string, public pr: number, public sha: string) {}
|
||||
}
|
||||
|
||||
export class CreatedBuildEvent extends BuildEvent {
|
||||
// Properties - Public, Static
|
||||
public static type = 'build.created';
|
||||
|
||||
// Constructor
|
||||
constructor(pr: number, sha: string) {
|
||||
super(CreatedBuildEvent.type, pr, sha);
|
||||
}
|
||||
}
|
@ -1,78 +0,0 @@
|
||||
// Imports
|
||||
import * as jwt from 'jsonwebtoken';
|
||||
import {GithubPullRequests} from '../common/github-pull-requests';
|
||||
import {GithubTeams} from '../common/github-teams';
|
||||
import {assertNotMissingOrEmpty} from '../common/utils';
|
||||
import {UploadError} from './upload-error';
|
||||
|
||||
// Interfaces - Types
|
||||
interface JwtPayload {
|
||||
slug: string;
|
||||
'pull-request': number;
|
||||
}
|
||||
|
||||
// Classes
|
||||
export class BuildVerifier {
|
||||
// Properties - Protected
|
||||
protected githubPullRequests: GithubPullRequests;
|
||||
protected githubTeams: GithubTeams;
|
||||
|
||||
// Constructor
|
||||
constructor(protected secret: string, githubToken: string, protected repoSlug: string, organization: string,
|
||||
protected allowedTeamSlugs: string[]) {
|
||||
assertNotMissingOrEmpty('secret', secret);
|
||||
assertNotMissingOrEmpty('githubToken', githubToken);
|
||||
assertNotMissingOrEmpty('repoSlug', repoSlug);
|
||||
assertNotMissingOrEmpty('organization', organization);
|
||||
assertNotMissingOrEmpty('allowedTeamSlugs', allowedTeamSlugs && allowedTeamSlugs.join(''));
|
||||
|
||||
this.githubPullRequests = new GithubPullRequests(githubToken, repoSlug);
|
||||
this.githubTeams = new GithubTeams(githubToken, organization);
|
||||
}
|
||||
|
||||
// Methods - Public
|
||||
public getPrAuthorTeamMembership(pr: number): Promise<{author: string, isMember: boolean}> {
|
||||
return Promise.resolve().
|
||||
then(() => this.githubPullRequests.fetch(pr)).
|
||||
then(prInfo => prInfo.user.login).
|
||||
then(author => this.githubTeams.isMemberBySlug(author, this.allowedTeamSlugs).
|
||||
then(isMember => ({author, isMember})));
|
||||
}
|
||||
|
||||
public verify(expectedPr: number, authHeader: string): Promise<void> {
|
||||
return Promise.resolve().
|
||||
then(() => this.extractJwtString(authHeader)).
|
||||
then(jwtString => this.verifyJwt(expectedPr, jwtString)).
|
||||
then(jwtPayload => this.verifyPr(jwtPayload['pull-request'])).
|
||||
catch(err => { throw new UploadError(403, `Error while verifying upload for PR ${expectedPr}: ${err}`); });
|
||||
}
|
||||
|
||||
// Methods - Protected
|
||||
protected extractJwtString(input: string): string {
|
||||
return input.replace(/^token +/i, '');
|
||||
}
|
||||
|
||||
protected verifyJwt(expectedPr: number, token: string): Promise<JwtPayload> {
|
||||
return new Promise((resolve, reject) => {
|
||||
jwt.verify(token, this.secret, {issuer: 'Travis CI, GmbH'}, (err, payload) => {
|
||||
if (err) {
|
||||
reject(err.message || err);
|
||||
} else if (payload.slug !== this.repoSlug) {
|
||||
reject(`jwt slug invalid. expected: ${this.repoSlug}`);
|
||||
} else if (payload['pull-request'] !== expectedPr) {
|
||||
reject(`jwt pull-request invalid. expected: ${expectedPr}`);
|
||||
} else {
|
||||
resolve(payload);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
protected verifyPr(pr: number): Promise<void> {
|
||||
return this.getPrAuthorTeamMembership(pr).
|
||||
then(({author, isMember}) => isMember ? Promise.resolve() : Promise.reject(
|
||||
`User '${author}' is not an active member of any of the following teams: ` +
|
||||
`${this.allowedTeamSlugs.join(', ')}`,
|
||||
));
|
||||
}
|
||||
}
|
@ -1,39 +0,0 @@
|
||||
// Imports
|
||||
import {getEnvVar} from '../common/utils';
|
||||
import {BuildVerifier} from './build-verifier';
|
||||
|
||||
// Run
|
||||
_main();
|
||||
|
||||
// Functions
|
||||
function _main() {
|
||||
const secret = 'unused';
|
||||
const githubToken = getEnvVar('AIO_GITHUB_TOKEN');
|
||||
const repoSlug = getEnvVar('AIO_REPO_SLUG');
|
||||
const organization = getEnvVar('AIO_GITHUB_ORGANIZATION');
|
||||
const allowedTeamSlugs = getEnvVar('AIO_GITHUB_TEAM_SLUGS').split(',');
|
||||
const pr = +getEnvVar('AIO_PREVERIFY_PR');
|
||||
|
||||
const buildVerifier = new BuildVerifier(secret, githubToken, repoSlug, organization, allowedTeamSlugs);
|
||||
|
||||
// Exit codes:
|
||||
// - 0: The PR author is a member.
|
||||
// - 1: The PR author is not a member.
|
||||
// - 2: An error occurred.
|
||||
buildVerifier.getPrAuthorTeamMembership(pr).
|
||||
then(({author, isMember}) => {
|
||||
if (isMember) {
|
||||
process.exit(0);
|
||||
} else {
|
||||
const errorMessage = `User '${author}' is not an active member of any of the following teams: ` +
|
||||
`${allowedTeamSlugs.join(', ')}`;
|
||||
onError(errorMessage, 1);
|
||||
}
|
||||
}).
|
||||
catch(err => onError(err, 2));
|
||||
}
|
||||
|
||||
function onError(err: string, exitCode: number) {
|
||||
console.error(err);
|
||||
process.exit(exitCode || 1);
|
||||
}
|
@ -1,10 +0,0 @@
|
||||
// Imports
|
||||
import {GithubPullRequests} from '../common/github-pull-requests';
|
||||
import {BuildVerifier} from './build-verifier';
|
||||
|
||||
// Run
|
||||
// TODO(gkalpak): Add e2e tests to cover these interactions as well.
|
||||
GithubPullRequests.prototype.addComment = () => Promise.resolve();
|
||||
BuildVerifier.prototype.verify = () => Promise.resolve();
|
||||
// tslint:disable-next-line: no-var-requires
|
||||
require('./index');
|
@ -1,35 +0,0 @@
|
||||
// TODO(gkalpak): Find more suitable way to run as `www-data`.
|
||||
process.setuid('www-data');
|
||||
|
||||
// Imports
|
||||
import {getEnvVar} from '../common/utils';
|
||||
import {uploadServerFactory} from './upload-server-factory';
|
||||
|
||||
// Constants
|
||||
const AIO_BUILDS_DIR = getEnvVar('AIO_BUILDS_DIR');
|
||||
const AIO_DOMAIN_NAME = getEnvVar('AIO_DOMAIN_NAME');
|
||||
const AIO_GITHUB_ORGANIZATION = getEnvVar('AIO_GITHUB_ORGANIZATION');
|
||||
const AIO_GITHUB_TEAM_SLUGS = getEnvVar('AIO_GITHUB_TEAM_SLUGS');
|
||||
const AIO_GITHUB_TOKEN = getEnvVar('AIO_GITHUB_TOKEN');
|
||||
const AIO_PREVIEW_DEPLOYMENT_TOKEN = getEnvVar('AIO_PREVIEW_DEPLOYMENT_TOKEN');
|
||||
const AIO_REPO_SLUG = getEnvVar('AIO_REPO_SLUG');
|
||||
const AIO_UPLOAD_HOSTNAME = getEnvVar('AIO_UPLOAD_HOSTNAME');
|
||||
const AIO_UPLOAD_PORT = +getEnvVar('AIO_UPLOAD_PORT');
|
||||
|
||||
// Run
|
||||
_main();
|
||||
|
||||
// Functions
|
||||
function _main() {
|
||||
uploadServerFactory.
|
||||
create({
|
||||
buildsDir: AIO_BUILDS_DIR,
|
||||
domainName: AIO_DOMAIN_NAME,
|
||||
githubOrganization: AIO_GITHUB_ORGANIZATION,
|
||||
githubTeamSlugs: AIO_GITHUB_TEAM_SLUGS.split(','),
|
||||
githubToken: AIO_GITHUB_TOKEN,
|
||||
repoSlug: AIO_REPO_SLUG,
|
||||
secret: AIO_PREVIEW_DEPLOYMENT_TOKEN,
|
||||
}).
|
||||
listen(AIO_UPLOAD_PORT, AIO_UPLOAD_HOSTNAME);
|
||||
}
|
@ -1,8 +0,0 @@
|
||||
// Classes
|
||||
export class UploadError extends Error {
|
||||
// Constructor
|
||||
constructor(public status: number = 500, message?: string) {
|
||||
super(message);
|
||||
Object.setPrototypeOf(this, UploadError.prototype);
|
||||
}
|
||||
}
|
@ -1,117 +0,0 @@
|
||||
// Imports
|
||||
import * as express from 'express';
|
||||
import * as http from 'http';
|
||||
import {GithubPullRequests} from '../common/github-pull-requests';
|
||||
import {assertNotMissingOrEmpty} from '../common/utils';
|
||||
import {BuildCreator} from './build-creator';
|
||||
import {CreatedBuildEvent} from './build-events';
|
||||
import {BuildVerifier} from './build-verifier';
|
||||
import {UploadError} from './upload-error';
|
||||
|
||||
// Constants
|
||||
const AUTHORIZATION_HEADER = 'AUTHORIZATION';
|
||||
const X_FILE_HEADER = 'X-FILE';
|
||||
|
||||
// Interfaces - Types
|
||||
interface UploadServerConfig {
|
||||
buildsDir: string;
|
||||
domainName: string;
|
||||
githubOrganization: string;
|
||||
githubTeamSlugs: string[];
|
||||
githubToken: string;
|
||||
repoSlug: string;
|
||||
secret: string;
|
||||
}
|
||||
|
||||
// Classes
|
||||
class UploadServerFactory {
|
||||
// Methods - Public
|
||||
public create({
|
||||
buildsDir,
|
||||
domainName,
|
||||
githubOrganization,
|
||||
githubTeamSlugs,
|
||||
githubToken,
|
||||
repoSlug,
|
||||
secret,
|
||||
}: UploadServerConfig): http.Server {
|
||||
assertNotMissingOrEmpty('domainName', domainName);
|
||||
|
||||
const buildVerifier = new BuildVerifier(secret, githubToken, repoSlug, githubOrganization, githubTeamSlugs);
|
||||
const buildCreator = this.createBuildCreator(buildsDir, githubToken, repoSlug, domainName);
|
||||
|
||||
const middleware = this.createMiddleware(buildVerifier, buildCreator);
|
||||
const httpServer = http.createServer(middleware);
|
||||
|
||||
httpServer.on('listening', () => {
|
||||
const info = httpServer.address();
|
||||
console.info(`Up and running (and listening on ${info.address}:${info.port})...`);
|
||||
});
|
||||
|
||||
return httpServer;
|
||||
}
|
||||
|
||||
// Methods - Protected
|
||||
protected createBuildCreator(buildsDir: string, githubToken: string, repoSlug: string,
|
||||
domainName: string): BuildCreator {
|
||||
const buildCreator = new BuildCreator(buildsDir);
|
||||
const githubPullRequests = new GithubPullRequests(githubToken, repoSlug);
|
||||
|
||||
buildCreator.on(CreatedBuildEvent.type, ({pr, sha}: CreatedBuildEvent) => {
|
||||
const body = `The angular.io preview for ${sha.slice(0, 7)} is available [here][1].\n\n` +
|
||||
`[1]: https://pr${pr}-${sha}.${domainName}/`;
|
||||
|
||||
githubPullRequests.addComment(pr, body);
|
||||
});
|
||||
|
||||
return buildCreator;
|
||||
}
|
||||
|
||||
protected createMiddleware(buildVerifier: BuildVerifier, buildCreator: BuildCreator): express.Express {
|
||||
const middleware = express();
|
||||
|
||||
middleware.get(/^\/create-build\/([1-9][0-9]*)\/([0-9a-f]{40})\/?$/, (req, res) => {
|
||||
const pr = req.params[0];
|
||||
const sha = req.params[1];
|
||||
const archive = req.header(X_FILE_HEADER);
|
||||
const authHeader = req.header(AUTHORIZATION_HEADER);
|
||||
|
||||
if (!authHeader) {
|
||||
this.throwRequestError(401, `Missing or empty '${AUTHORIZATION_HEADER}' header`, req);
|
||||
} else if (!archive) {
|
||||
this.throwRequestError(400, `Missing or empty '${X_FILE_HEADER}' header`, req);
|
||||
}
|
||||
|
||||
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('*', req => this.throwRequestError(404, 'Unknown resource', req));
|
||||
middleware.all('*', req => this.throwRequestError(405, 'Unsupported method', req));
|
||||
middleware.use((err: any, _req: any, res: express.Response, _next: any) => this.respondWithError(res, err));
|
||||
|
||||
return middleware;
|
||||
}
|
||||
|
||||
protected respondWithError(res: express.Response, err: any) {
|
||||
if (!(err instanceof UploadError)) {
|
||||
err = new UploadError(500, String((err && err.message) || err));
|
||||
}
|
||||
|
||||
const statusText = http.STATUS_CODES[err.status] || '???';
|
||||
console.error(`Upload error: ${err.status} - ${statusText}`);
|
||||
console.error(err.message);
|
||||
|
||||
res.status(err.status).end(err.message);
|
||||
}
|
||||
|
||||
protected throwRequestError(status: number, error: string, req: express.Request) {
|
||||
throw new UploadError(status, `${error} in request: ${req.method} ${req.originalUrl}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Exports
|
||||
export const uploadServerFactory = new UploadServerFactory();
|
@ -1,191 +0,0 @@
|
||||
// Imports
|
||||
import * as cp from 'child_process';
|
||||
import * as fs from 'fs';
|
||||
import * as http from 'http';
|
||||
import * as path from 'path';
|
||||
import * as shell from 'shelljs';
|
||||
import {getEnvVar} from '../common/utils';
|
||||
|
||||
// Constans
|
||||
const SERVER_USER = 'www-data';
|
||||
const TEST_AIO_BUILDS_DIR = getEnvVar('TEST_AIO_BUILDS_DIR');
|
||||
const TEST_AIO_NGINX_HOSTNAME = getEnvVar('TEST_AIO_NGINX_HOSTNAME');
|
||||
const TEST_AIO_NGINX_PORT_HTTP = +getEnvVar('TEST_AIO_NGINX_PORT_HTTP');
|
||||
const TEST_AIO_NGINX_PORT_HTTPS = +getEnvVar('TEST_AIO_NGINX_PORT_HTTPS');
|
||||
const TEST_AIO_UPLOAD_HOSTNAME = getEnvVar('TEST_AIO_UPLOAD_HOSTNAME');
|
||||
const TEST_AIO_UPLOAD_MAX_SIZE = +getEnvVar('TEST_AIO_UPLOAD_MAX_SIZE');
|
||||
const TEST_AIO_UPLOAD_PORT = +getEnvVar('TEST_AIO_UPLOAD_PORT');
|
||||
|
||||
// Interfaces - Types
|
||||
export interface CmdResult { success: boolean; err: Error; stdout: string; stderr: string; }
|
||||
export interface FileSpecs { content?: string; size?: number; }
|
||||
|
||||
export type CleanUpFn = () => void;
|
||||
export type TestSuiteFactory = (scheme: string, port: number) => void;
|
||||
export type VerifyCmdResultFn = (result: CmdResult) => void;
|
||||
|
||||
// Classes
|
||||
class Helper {
|
||||
// Properties - Public
|
||||
public get buildsDir() { return TEST_AIO_BUILDS_DIR; }
|
||||
public get nginxHostname() { return TEST_AIO_NGINX_HOSTNAME; }
|
||||
public get nginxPortHttp() { return TEST_AIO_NGINX_PORT_HTTP; }
|
||||
public get nginxPortHttps() { return TEST_AIO_NGINX_PORT_HTTPS; }
|
||||
public get serverUser() { return SERVER_USER; }
|
||||
public get uploadHostname() { return TEST_AIO_UPLOAD_HOSTNAME; }
|
||||
public get uploadPort() { return TEST_AIO_UPLOAD_PORT; }
|
||||
public get uploadMaxSize() { return TEST_AIO_UPLOAD_MAX_SIZE; }
|
||||
|
||||
// Properties - Protected
|
||||
protected cleanUpFns: CleanUpFn[] = [];
|
||||
protected portPerScheme: {[scheme: string]: number} = {
|
||||
http: this.nginxPortHttp,
|
||||
https: this.nginxPortHttps,
|
||||
};
|
||||
|
||||
// Constructor
|
||||
constructor() {
|
||||
shell.mkdir('-p', this.buildsDir);
|
||||
shell.exec(`chown -R ${this.serverUser} ${this.buildsDir}`);
|
||||
}
|
||||
|
||||
// Methods - Public
|
||||
public cleanUp() {
|
||||
while (this.cleanUpFns.length) {
|
||||
// Clean-up fns remove themselves from the list.
|
||||
this.cleanUpFns[0]();
|
||||
}
|
||||
|
||||
if (fs.readdirSync(this.buildsDir).length) {
|
||||
throw new Error(`Directory '${this.buildsDir}' is not empty after clean-up.`);
|
||||
}
|
||||
}
|
||||
|
||||
public createDummyArchive(pr: string, sha: string, archivePath: string): CleanUpFn {
|
||||
const inputDir = path.join(this.buildsDir, 'uploaded', pr, sha);
|
||||
const cmd1 = `tar --create --gzip --directory "${inputDir}" --file "${archivePath}" .`;
|
||||
const cmd2 = `chown ${this.serverUser} ${archivePath}`;
|
||||
|
||||
const cleanUpTemp = this.createDummyBuild(`uploaded/${pr}`, sha, true);
|
||||
shell.exec(cmd1);
|
||||
shell.exec(cmd2);
|
||||
cleanUpTemp();
|
||||
|
||||
return this.createCleanUpFn(() => shell.rm('-rf', archivePath));
|
||||
}
|
||||
|
||||
public createDummyBuild(pr: string, sha: string, force = false): CleanUpFn {
|
||||
const prDir = path.join(this.buildsDir, pr);
|
||||
const shaDir = path.join(prDir, sha);
|
||||
const idxPath = path.join(shaDir, 'index.html');
|
||||
const barPath = path.join(shaDir, 'foo', 'bar.js');
|
||||
|
||||
this.writeFile(idxPath, {content: `PR: ${pr} | SHA: ${sha} | File: /index.html`}, force);
|
||||
this.writeFile(barPath, {content: `PR: ${pr} | SHA: ${sha} | File: /foo/bar.js`}, force);
|
||||
shell.exec(`chown -R ${this.serverUser} ${prDir}`);
|
||||
|
||||
return this.createCleanUpFn(() => shell.rm('-rf', prDir));
|
||||
}
|
||||
|
||||
public deletePrDir(pr: string) {
|
||||
const prDir = path.join(this.buildsDir, pr);
|
||||
|
||||
if (fs.existsSync(prDir)) {
|
||||
// Undocumented signature (see https://github.com/shelljs/shelljs/pull/663).
|
||||
(shell as any).chmod('-R', 'a+w', prDir);
|
||||
shell.rm('-rf', prDir);
|
||||
}
|
||||
}
|
||||
|
||||
public readBuildFile(pr: string, sha: string, relFilePath: string): string {
|
||||
const absFilePath = path.join(this.buildsDir, pr, sha, relFilePath);
|
||||
return fs.readFileSync(absFilePath, 'utf8');
|
||||
}
|
||||
|
||||
public runCmd(cmd: string, opts: cp.ExecFileOptions = {}): Promise<CmdResult> {
|
||||
return new Promise(resolve => {
|
||||
const proc = cp.exec(cmd, opts, (err, stdout, stderr) => resolve({success: !err, err, stdout, stderr}));
|
||||
this.createCleanUpFn(() => proc.kill());
|
||||
});
|
||||
}
|
||||
|
||||
public runForAllSupportedSchemes(suiteFactory: TestSuiteFactory) {
|
||||
Object.keys(this.portPerScheme).forEach(scheme => suiteFactory(scheme, this.portPerScheme[scheme]));
|
||||
}
|
||||
|
||||
public verifyResponse(status: number | [number, string], regex = /^/): VerifyCmdResultFn {
|
||||
let statusCode: number;
|
||||
let statusText: string;
|
||||
|
||||
if (Array.isArray(status)) {
|
||||
statusCode = status[0];
|
||||
statusText = status[1];
|
||||
} else {
|
||||
statusCode = status;
|
||||
statusText = http.STATUS_CODES[statusCode];
|
||||
}
|
||||
|
||||
return (result: CmdResult) => {
|
||||
const [headers, body] = result.stdout.
|
||||
split(/(?:\r?\n){2,}/).
|
||||
map(s => s.trim()).
|
||||
slice(-2);
|
||||
|
||||
if (!result.success) {
|
||||
console.log('Stdout:', result.stdout);
|
||||
console.log('Stderr:', result.stderr);
|
||||
console.log('Error:', result.err);
|
||||
}
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(headers).toContain(`${statusCode} ${statusText}`);
|
||||
expect(body).toMatch(regex);
|
||||
};
|
||||
}
|
||||
|
||||
public writeBuildFile(pr: string, sha: string, relFilePath: string, content: string): CleanUpFn {
|
||||
const absFilePath = path.join(this.buildsDir, pr, sha, relFilePath);
|
||||
return this.writeFile(absFilePath, {content}, true);
|
||||
}
|
||||
|
||||
public writeFile(filePath: string, {content, size}: FileSpecs, force = false): CleanUpFn {
|
||||
if (!force && fs.existsSync(filePath)) {
|
||||
throw new Error(`Refusing to overwrite existing file '${filePath}'.`);
|
||||
}
|
||||
|
||||
let cleanUpTarget = filePath;
|
||||
while (!fs.existsSync(path.dirname(cleanUpTarget))) {
|
||||
cleanUpTarget = path.dirname(cleanUpTarget);
|
||||
}
|
||||
|
||||
shell.mkdir('-p', path.dirname(filePath));
|
||||
if (size) {
|
||||
// Create a file of the specified size.
|
||||
cp.execSync(`fallocate -l ${size} ${filePath}`);
|
||||
} else {
|
||||
// Create a file with the specified content.
|
||||
fs.writeFileSync(filePath, content || '');
|
||||
}
|
||||
shell.exec(`chown ${this.serverUser} ${filePath}`);
|
||||
|
||||
return this.createCleanUpFn(() => shell.rm('-rf', cleanUpTarget));
|
||||
}
|
||||
|
||||
// Methods - Protected
|
||||
protected createCleanUpFn(fn: Function): CleanUpFn {
|
||||
const cleanUpFn = () => {
|
||||
const idx = this.cleanUpFns.indexOf(cleanUpFn);
|
||||
if (idx !== -1) {
|
||||
this.cleanUpFns.splice(idx, 1);
|
||||
fn();
|
||||
}
|
||||
};
|
||||
|
||||
this.cleanUpFns.push(cleanUpFn);
|
||||
|
||||
return cleanUpFn;
|
||||
}
|
||||
}
|
||||
|
||||
// Exports
|
||||
export const helper = new Helper();
|
@ -1,6 +0,0 @@
|
||||
// Imports
|
||||
import {runTests} from '../common/run-tests';
|
||||
|
||||
// Run
|
||||
const specFiles = [`${__dirname}/**/*.e2e.js`];
|
||||
runTests(specFiles);
|
@ -1,271 +0,0 @@
|
||||
// Imports
|
||||
import * as path from 'path';
|
||||
import {helper as h} from './helper';
|
||||
|
||||
// Tests
|
||||
describe(`nginx`, () => {
|
||||
|
||||
beforeEach(() => jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000);
|
||||
afterEach(() => h.cleanUp());
|
||||
|
||||
|
||||
it('should redirect HTTP to HTTPS', done => {
|
||||
const httpHost = `${h.nginxHostname}:${h.nginxPortHttp}`;
|
||||
const httpsHost = `${h.nginxHostname}:${h.nginxPortHttps}`;
|
||||
const urlMap = {
|
||||
[`http://${httpHost}/`]: `https://${httpsHost}/`,
|
||||
[`http://${httpHost}/foo`]: `https://${httpsHost}/foo`,
|
||||
[`http://foo.${httpHost}/`]: `https://foo.${httpsHost}/`,
|
||||
};
|
||||
|
||||
const verifyRedirection = (httpUrl: string) => h.runCmd(`curl -i ${httpUrl}`).then(result => {
|
||||
h.verifyResponse(307)(result);
|
||||
|
||||
const headers = result.stdout.split(/(?:\r?\n){2,}/)[0];
|
||||
expect(headers).toContain(`Location: ${urlMap[httpUrl]}`);
|
||||
});
|
||||
|
||||
Promise.
|
||||
all(Object.keys(urlMap).map(verifyRedirection)).
|
||||
then(done);
|
||||
});
|
||||
|
||||
|
||||
h.runForAllSupportedSchemes((scheme, port) => describe(`nginx (on ${scheme.toUpperCase()})`, () => {
|
||||
const hostname = h.nginxHostname;
|
||||
const host = `${hostname}:${port}`;
|
||||
const pr = '9';
|
||||
const sha9 = '9'.repeat(40);
|
||||
const sha0 = '0'.repeat(40);
|
||||
|
||||
|
||||
describe(`pr<pr>-<sha>.${host}/*`, () => {
|
||||
|
||||
beforeEach(() => {
|
||||
h.createDummyBuild(pr, sha9);
|
||||
h.createDummyBuild(pr, sha0);
|
||||
});
|
||||
|
||||
|
||||
it('should return /index.html', done => {
|
||||
const origin = `${scheme}://pr${pr}-${sha9}.${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 /foo/bar.js', done => {
|
||||
const bodyRegex = new RegExp(`^PR: ${pr} | SHA: ${sha9} | File: /foo/bar\\.js$`);
|
||||
|
||||
h.runCmd(`curl -iL ${scheme}://pr${pr}-${sha9}.${host}/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}-${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);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
describe(`${host}/health-check`, () => {
|
||||
|
||||
it('should respond with 200', done => {
|
||||
Promise.all([
|
||||
h.runCmd(`curl -iL ${scheme}://${host}/health-check`).then(h.verifyResponse(200)),
|
||||
h.runCmd(`curl -iL ${scheme}://${host}/health-check/`).then(h.verifyResponse(200)),
|
||||
]).then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should respond with 404 if the path does not match exactly', done => {
|
||||
Promise.all([
|
||||
h.runCmd(`curl -iL ${scheme}://${host}/health-check/foo`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`curl -iL ${scheme}://${host}/health-check-foo`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`curl -iL ${scheme}://${host}/health-checknfoo`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`curl -iL ${scheme}://${host}/foo/health-check`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`curl -iL ${scheme}://${host}/foo-health-check`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`curl -iL ${scheme}://${host}/foonhealth-check`).then(h.verifyResponse(404)),
|
||||
]).then(done);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
describe(`${host}/create-build/<pr>/<sha>`, () => {
|
||||
|
||||
it('should disallow non-POST requests', done => {
|
||||
const url = `${scheme}://${host}/create-build/${pr}/${sha9}`;
|
||||
|
||||
Promise.all([
|
||||
h.runCmd(`curl -iLX GET ${url}`).then(h.verifyResponse([405, 'Not Allowed'])),
|
||||
h.runCmd(`curl -iLX PUT ${url}`).then(h.verifyResponse([405, 'Not Allowed'])),
|
||||
h.runCmd(`curl -iLX PATCH ${url}`).then(h.verifyResponse([405, 'Not Allowed'])),
|
||||
h.runCmd(`curl -iLX DELETE ${url}`).then(h.verifyResponse([405, 'Not Allowed'])),
|
||||
]).then(done);
|
||||
});
|
||||
|
||||
|
||||
it(`should reject files larger than ${h.uploadMaxSize}B (according to header)`, done => {
|
||||
const headers = `--header "Content-Length: ${1.5 * h.uploadMaxSize}"`;
|
||||
const url = `${scheme}://${host}/create-build/${pr}/${sha9}`;
|
||||
|
||||
h.runCmd(`curl -iLX POST ${headers} ${url}`).
|
||||
then(h.verifyResponse([413, 'Request Entity Too Large'])).
|
||||
then(done);
|
||||
});
|
||||
|
||||
|
||||
it(`should reject files larger than ${h.uploadMaxSize}B (without header)`, done => {
|
||||
const filePath = path.join(h.buildsDir, 'snapshot.tar.gz');
|
||||
const url = `${scheme}://${host}/create-build/${pr}/${sha9}`;
|
||||
|
||||
h.writeFile(filePath, {size: 1.5 * h.uploadMaxSize});
|
||||
|
||||
h.runCmd(`curl -iLX POST --data-binary "@${filePath}" ${url}`).
|
||||
then(h.verifyResponse([413, 'Request Entity Too Large'])).
|
||||
then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should pass requests through to the upload server', done => {
|
||||
h.runCmd(`curl -iLX POST ${scheme}://${host}/create-build/${pr}/${sha9}`).
|
||||
then(h.verifyResponse(401, /Missing or empty 'AUTHORIZATION' header/)).
|
||||
then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should respond with 404 for unknown paths', done => {
|
||||
const cmdPrefix = `curl -iLX POST ${scheme}://${host}`;
|
||||
|
||||
Promise.all([
|
||||
h.runCmd(`${cmdPrefix}/foo/create-build/${pr}/${sha9}`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`${cmdPrefix}/foo-create-build/${pr}/${sha9}`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`${cmdPrefix}/fooncreate-build/${pr}/${sha9}`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`${cmdPrefix}/create-build/foo/${pr}/${sha9}`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`${cmdPrefix}/create-build-foo/${pr}/${sha9}`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`${cmdPrefix}/create-buildnfoo/${pr}/${sha9}`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`${cmdPrefix}/create-build/pr${pr}/${sha9}`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`${cmdPrefix}/create-build/${pr}/${sha9}42`).then(h.verifyResponse(404)),
|
||||
]).then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should reject PRs with leading zeros', done => {
|
||||
h.runCmd(`curl -iLX POST ${scheme}://${host}/create-build/0${pr}/${sha9}`).
|
||||
then(h.verifyResponse(404)).
|
||||
then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should accept SHAs with leading zeros (but not trim the zeros)', done => {
|
||||
const cmdPrefix = `curl -iLX POST ${scheme}://${host}/create-build/${pr}`;
|
||||
const bodyRegex = /Missing or empty 'AUTHORIZATION' header/;
|
||||
|
||||
Promise.all([
|
||||
h.runCmd(`${cmdPrefix}/0${sha9}`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`${cmdPrefix}/${sha0}`).then(h.verifyResponse(401, bodyRegex)),
|
||||
]).then(done);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
describe(`${host}/*`, () => {
|
||||
|
||||
it('should respond with 404 for unkown URLs (even if the resource exists)', done => {
|
||||
['index.html', 'foo.js', 'foo/index.html'].forEach(relFilePath => {
|
||||
const absFilePath = path.join(h.buildsDir, relFilePath);
|
||||
h.writeFile(absFilePath, {content: `File: /${relFilePath}`});
|
||||
});
|
||||
|
||||
Promise.all([
|
||||
h.runCmd(`curl -iL ${scheme}://${host}/index.html`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`curl -iL ${scheme}://${host}/`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`curl -iL ${scheme}://${host}`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`curl -iL ${scheme}://foo.${host}/index.html`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`curl -iL ${scheme}://foo.${host}/`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`curl -iL ${scheme}://foo.${host}`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`curl -iL ${scheme}://${host}/foo.js`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`curl -iL ${scheme}://${host}/foo/index.html`).then(h.verifyResponse(404)),
|
||||
]).then(done);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
}));
|
||||
|
||||
});
|
@ -1,84 +0,0 @@
|
||||
// Imports
|
||||
import * as path from 'path';
|
||||
import {helper as h} from './helper';
|
||||
|
||||
// Tests
|
||||
h.runForAllSupportedSchemes((scheme, port) => describe(`integration (on ${scheme.toUpperCase()})`, () => {
|
||||
const hostname = h.nginxHostname;
|
||||
const host = `${hostname}:${port}`;
|
||||
const pr9 = '9';
|
||||
const sha9 = '9'.repeat(40);
|
||||
const sha0 = '0'.repeat(40);
|
||||
const archivePath = path.join(h.buildsDir, 'snapshot.tar.gz');
|
||||
|
||||
const getFile = (pr: string, sha: string, file: string) =>
|
||||
h.runCmd(`curl -iL ${scheme}://pr${pr}-${sha}.${host}/${file}`);
|
||||
const uploadBuild = (pr: string, sha: string, archive: string) => {
|
||||
const curlPost = 'curl -iLX POST --header "Authorization: Token FOO"';
|
||||
return h.runCmd(`${curlPost} --data-binary "@${archive}" ${scheme}://${host}/create-build/${pr}/${sha}`);
|
||||
};
|
||||
|
||||
beforeEach(() => jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000);
|
||||
afterEach(() => {
|
||||
h.deletePrDir(pr9);
|
||||
h.cleanUp();
|
||||
});
|
||||
|
||||
|
||||
it('should be able to upload and serve a build for a new PR', 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 and serve a build for an existing PR', done => {
|
||||
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:`;
|
||||
const idxContentRegex9 = new RegExp(`${regexPrefix9} \\/index\\.html$`);
|
||||
const barContentRegex9 = new RegExp(`${regexPrefix9} \\/foo\\/bar\\.js$`);
|
||||
|
||||
h.createDummyBuild(pr9, sha0);
|
||||
h.createDummyArchive(pr9, sha9, archivePath);
|
||||
|
||||
uploadBuild(pr9, sha9, archivePath).
|
||||
then(() => Promise.all([
|
||||
getFile(pr9, sha0, 'index.html').then(h.verifyResponse(200, idxContentRegex0)),
|
||||
getFile(pr9, sha0, 'foo/bar.js').then(h.verifyResponse(200, barContentRegex0)),
|
||||
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 => {
|
||||
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(403)).
|
||||
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);
|
||||
});
|
||||
|
||||
}));
|
@ -1,266 +0,0 @@
|
||||
// Imports
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import {CmdResult, helper as h} from './helper';
|
||||
|
||||
// Tests
|
||||
describe('upload-server (on HTTP)', () => {
|
||||
const hostname = h.uploadHostname;
|
||||
const port = h.uploadPort;
|
||||
const host = `${hostname}:${port}`;
|
||||
const pr = '9';
|
||||
const sha9 = '9'.repeat(40);
|
||||
const sha0 = '0'.repeat(40);
|
||||
|
||||
beforeEach(() => jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000);
|
||||
afterEach(() => h.cleanUp());
|
||||
|
||||
|
||||
describe(`${host}/create-build/<pr>/<sha>`, () => {
|
||||
const authorizationHeader = `--header "Authorization: Token FOO"`;
|
||||
const xFileHeader = `--header "X-File: ${h.buildsDir}/snapshot.tar.gz"`;
|
||||
const curl = `curl -iL ${authorizationHeader} ${xFileHeader}`;
|
||||
|
||||
|
||||
it('should disallow non-GET requests', done => {
|
||||
const url = `http://${host}/create-build/${pr}/${sha9}`;
|
||||
const bodyRegex = /^Unsupported method/;
|
||||
|
||||
Promise.all([
|
||||
h.runCmd(`curl -iLX PUT ${url}`).then(h.verifyResponse(405, bodyRegex)),
|
||||
h.runCmd(`curl -iLX POST ${url}`).then(h.verifyResponse(405, bodyRegex)),
|
||||
h.runCmd(`curl -iLX PATCH ${url}`).then(h.verifyResponse(405, bodyRegex)),
|
||||
h.runCmd(`curl -iLX DELETE ${url}`).then(h.verifyResponse(405, bodyRegex)),
|
||||
]).then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should reject requests without an \'AUTHORIZATION\' header', done => {
|
||||
const headers1 = '';
|
||||
const headers2 = '--header "AUTHORIXATION: "';
|
||||
const url = `http://${host}/create-build/${pr}/${sha9}`;
|
||||
const bodyRegex = /^Missing or empty 'AUTHORIZATION' header/;
|
||||
|
||||
Promise.all([
|
||||
h.runCmd(`curl -iL ${headers1} ${url}`).then(h.verifyResponse(401, bodyRegex)),
|
||||
h.runCmd(`curl -iL ${headers2} ${url}`).then(h.verifyResponse(401, bodyRegex)),
|
||||
]).then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should reject requests without an \'X-FILE\' header', done => {
|
||||
const headers1 = authorizationHeader;
|
||||
const headers2 = `${authorizationHeader} --header "X-FILE: "`;
|
||||
const url = `http://${host}/create-build/${pr}/${sha9}`;
|
||||
const bodyRegex = /^Missing or empty 'X-FILE' header/;
|
||||
|
||||
Promise.all([
|
||||
h.runCmd(`curl -iL ${headers1} ${url}`).then(h.verifyResponse(400, bodyRegex)),
|
||||
h.runCmd(`curl -iL ${headers2} ${url}`).then(h.verifyResponse(400, bodyRegex)),
|
||||
]).then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should respond with 404 for unknown paths', done => {
|
||||
const cmdPrefix = `${curl} http://${host}`;
|
||||
|
||||
Promise.all([
|
||||
h.runCmd(`${cmdPrefix}/foo/create-build/${pr}/${sha9}`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`${cmdPrefix}/foo-create-build/${pr}/${sha9}`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`${cmdPrefix}/fooncreate-build/${pr}/${sha9}`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`${cmdPrefix}/create-build/foo/${pr}/${sha9}`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`${cmdPrefix}/create-build-foo/${pr}/${sha9}`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`${cmdPrefix}/create-buildnfoo/${pr}/${sha9}`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`${cmdPrefix}/create-build/pr${pr}/${sha9}`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`${cmdPrefix}/create-build/${pr}/${sha9}42`).then(h.verifyResponse(404)),
|
||||
]).then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should reject PRs with leading zeros', done => {
|
||||
h.runCmd(`${curl} http://${host}/create-build/0${pr}/${sha9}`).
|
||||
then(h.verifyResponse(404)).
|
||||
then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should accept SHAs with leading zeros (but not trim the zeros)', done => {
|
||||
Promise.all([
|
||||
h.runCmd(`${curl} http://${host}/create-build/${pr}/0${sha9}`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`${curl} http://${host}/create-build/${pr}/${sha9}`).then(h.verifyResponse(500)),
|
||||
h.runCmd(`${curl} http://${host}/create-build/${pr}/${sha0}`).then(h.verifyResponse(500)),
|
||||
]).then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should not overwrite existing builds', done => {
|
||||
h.createDummyBuild(pr, sha9);
|
||||
expect(h.readBuildFile(pr, sha9, 'index.html')).toContain('index.html');
|
||||
|
||||
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(403, /^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 => {
|
||||
const prDir = path.join(h.buildsDir, pr);
|
||||
|
||||
h.runCmd(`${curl} http://${host}/create-build/${pr}/${sha9}`).
|
||||
then(h.verifyResponse(500)).
|
||||
then(() => expect(fs.existsSync(prDir)).toBe(false)).
|
||||
then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should only delete the SHA directory on error (for existing PR)', done => {
|
||||
const prDir = path.join(h.buildsDir, pr);
|
||||
const shaDir = path.join(prDir, sha9);
|
||||
|
||||
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 => {
|
||||
uploadPromise.
|
||||
then(() => {
|
||||
expect(h.readBuildFile(pr, sha9, 'index.html')).toContain(`uploaded/${pr}`);
|
||||
expect(h.readBuildFile(pr, sha9, 'foo/bar.js')).toContain(`uploaded/${pr}`);
|
||||
}).
|
||||
then(done);
|
||||
});
|
||||
|
||||
|
||||
it(`should create files/directories owned by '${h.serverUser}'`, done => {
|
||||
const shaDir = path.join(h.buildsDir, pr, 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.serverUser}`),
|
||||
])).
|
||||
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 shaDir = path.join(h.buildsDir, pr, 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);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
describe(`${host}/health-check`, () => {
|
||||
|
||||
it('should respond with 200', done => {
|
||||
Promise.all([
|
||||
h.runCmd(`curl -iL http://${host}/health-check`).then(h.verifyResponse(200)),
|
||||
h.runCmd(`curl -iL http://${host}/health-check/`).then(h.verifyResponse(200)),
|
||||
]).then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should respond with 404 if the path does not match exactly', done => {
|
||||
Promise.all([
|
||||
h.runCmd(`curl -iL http://${host}/health-check/foo`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`curl -iL http://${host}/health-check-foo`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`curl -iL http://${host}/health-checknfoo`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`curl -iL http://${host}/foo/health-check`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`curl -iL http://${host}/foo-health-check`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`curl -iL http://${host}/foonhealth-check`).then(h.verifyResponse(404)),
|
||||
]).then(done);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
describe(`${host}/*`, () => {
|
||||
|
||||
it('should respond with 404 for GET requests to unknown URLs', done => {
|
||||
const bodyRegex = /^Unknown resource/;
|
||||
|
||||
Promise.all([
|
||||
h.runCmd(`curl -iL http://${host}/index.html`).then(h.verifyResponse(404, bodyRegex)),
|
||||
h.runCmd(`curl -iL http://${host}/`).then(h.verifyResponse(404, bodyRegex)),
|
||||
h.runCmd(`curl -iL http://${host}`).then(h.verifyResponse(404, bodyRegex)),
|
||||
]).then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should respond with 405 for non-GET requests to any URL', done => {
|
||||
const bodyRegex = /^Unsupported method/;
|
||||
|
||||
Promise.all([
|
||||
h.runCmd(`curl -iLX PUT http://${host}`).then(h.verifyResponse(405, bodyRegex)),
|
||||
h.runCmd(`curl -iLX POST http://${host}`).then(h.verifyResponse(405, bodyRegex)),
|
||||
h.runCmd(`curl -iLX PATCH http://${host}`).then(h.verifyResponse(405, bodyRegex)),
|
||||
h.runCmd(`curl -iLX DELETE http://${host}`).then(h.verifyResponse(405, bodyRegex)),
|
||||
]).then(done);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
@ -1,43 +0,0 @@
|
||||
{
|
||||
"name": "aio-scripts-js",
|
||||
"version": "1.0.0",
|
||||
"description": "Performing various tasks on PR build artifacts for angular.io.",
|
||||
"repository": "https://github.com/angular/angular.git",
|
||||
"author": "Angular",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"prebuild": "yarn run clean",
|
||||
"build": "tsc",
|
||||
"build-watch": "yarn run tsc -- --watch",
|
||||
"clean": "node --eval \"require('shelljs').rm('-rf', 'dist')\"",
|
||||
"dev": "concurrently --kill-others --raw --success first \"yarn run build-watch\" \"yarn run test-watch\"",
|
||||
"lint": "tslint --project tsconfig.json",
|
||||
"pre~~test-only": "yarn run lint",
|
||||
"~~test-only": "node dist/test",
|
||||
"pretest": "yarn run build",
|
||||
"test": "yarn run ~~test-only",
|
||||
"pretest-watch": "yarn run build",
|
||||
"test-watch": "nodemon --exec \"yarn run ~~test-only\" --watch dist"
|
||||
},
|
||||
"dependencies": {
|
||||
"express": "^4.14.1",
|
||||
"jasmine": "^2.5.3",
|
||||
"jsonwebtoken": "^7.3.0",
|
||||
"shelljs": "^0.7.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/express": "^4.0.35",
|
||||
"@types/jasmine": "^2.5.43",
|
||||
"@types/jsonwebtoken": "^7.2.0",
|
||||
"@types/node": "^7.0.5",
|
||||
"@types/shelljs": "^0.7.0",
|
||||
"@types/supertest": "^2.0.0",
|
||||
"concurrently": "^3.3.0",
|
||||
"eslint": "^3.15.0",
|
||||
"eslint-plugin-jasmine": "^2.2.0",
|
||||
"nodemon": "^1.11.0",
|
||||
"supertest": "^3.0.0",
|
||||
"tslint": "^4.4.2",
|
||||
"typescript": "^2.1.6"
|
||||
}
|
||||
}
|
@ -1,318 +0,0 @@
|
||||
// Imports
|
||||
import * as fs from 'fs';
|
||||
import * as shell from 'shelljs';
|
||||
import {BuildCleaner} from '../../lib/clean-up/build-cleaner';
|
||||
import {GithubPullRequests} from '../../lib/common/github-pull-requests';
|
||||
|
||||
// Tests
|
||||
describe('BuildCleaner', () => {
|
||||
let cleaner: BuildCleaner;
|
||||
|
||||
beforeEach(() => cleaner = new BuildCleaner('/foo/bar', 'baz/qux', '12345'));
|
||||
|
||||
|
||||
describe('constructor()', () => {
|
||||
|
||||
it('should throw if \'buildsDir\' is empty', () => {
|
||||
expect(() => new BuildCleaner('', '/baz/qux', '12345')).
|
||||
toThrowError('Missing or empty required parameter \'buildsDir\'!');
|
||||
});
|
||||
|
||||
|
||||
it('should throw if \'repoSlug\' is empty', () => {
|
||||
expect(() => new BuildCleaner('/foo/bar', '', '12345')).
|
||||
toThrowError('Missing or empty required parameter \'repoSlug\'!');
|
||||
});
|
||||
|
||||
|
||||
it('should throw if \'githubToken\' is empty', () => {
|
||||
expect(() => new BuildCleaner('/foo/bar', 'baz/qux', '')).
|
||||
toThrowError('Missing or empty required parameter \'githubToken\'!');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
describe('cleanUp()', () => {
|
||||
let cleanerGetExistingBuildNumbersSpy: jasmine.Spy;
|
||||
let cleanerGetOpenPrNumbersSpy: jasmine.Spy;
|
||||
let cleanerRemoveUnnecessaryBuildsSpy: jasmine.Spy;
|
||||
let existingBuildsDeferred: {resolve: Function, reject: Function};
|
||||
let openPrsDeferred: {resolve: Function, reject: Function};
|
||||
let promise: Promise<void>;
|
||||
|
||||
beforeEach(() => {
|
||||
cleanerGetExistingBuildNumbersSpy = spyOn(cleaner as any, 'getExistingBuildNumbers').and.callFake(() => {
|
||||
return new Promise((resolve, reject) => existingBuildsDeferred = {resolve, reject});
|
||||
});
|
||||
cleanerGetOpenPrNumbersSpy = spyOn(cleaner as any, 'getOpenPrNumbers').and.callFake(() => {
|
||||
return new Promise((resolve, reject) => openPrsDeferred = {resolve, reject});
|
||||
});
|
||||
cleanerRemoveUnnecessaryBuildsSpy = spyOn(cleaner as any, 'removeUnnecessaryBuilds');
|
||||
|
||||
promise = cleaner.cleanUp();
|
||||
});
|
||||
|
||||
|
||||
it('should return a promise', () => {
|
||||
expect(promise).toEqual(jasmine.any(Promise));
|
||||
});
|
||||
|
||||
|
||||
it('should get the existing builds', () => {
|
||||
expect(cleanerGetExistingBuildNumbersSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
|
||||
it('should get the open PRs', () => {
|
||||
expect(cleanerGetOpenPrNumbersSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
|
||||
it('should reject if \'getExistingBuildNumbers()\' rejects', done => {
|
||||
promise.catch(err => {
|
||||
expect(err).toBe('Test');
|
||||
done();
|
||||
});
|
||||
|
||||
existingBuildsDeferred.reject('Test');
|
||||
});
|
||||
|
||||
|
||||
it('should reject if \'getOpenPrNumbers()\' rejects', done => {
|
||||
promise.catch(err => {
|
||||
expect(err).toBe('Test');
|
||||
done();
|
||||
});
|
||||
|
||||
openPrsDeferred.reject('Test');
|
||||
});
|
||||
|
||||
|
||||
it('should reject if \'removeUnnecessaryBuilds()\' rejects', done => {
|
||||
promise.catch(err => {
|
||||
expect(err).toBe('Test');
|
||||
done();
|
||||
});
|
||||
|
||||
cleanerRemoveUnnecessaryBuildsSpy.and.returnValue(Promise.reject('Test'));
|
||||
existingBuildsDeferred.resolve();
|
||||
openPrsDeferred.resolve();
|
||||
});
|
||||
|
||||
|
||||
it('should pass existing builds and open PRs to \'removeUnnecessaryBuilds()\'', done => {
|
||||
promise.then(() => {
|
||||
expect(cleanerRemoveUnnecessaryBuildsSpy).toHaveBeenCalledWith('foo', 'bar');
|
||||
done();
|
||||
});
|
||||
|
||||
existingBuildsDeferred.resolve('foo');
|
||||
openPrsDeferred.resolve('bar');
|
||||
});
|
||||
|
||||
|
||||
it('should resolve with the value returned by \'removeUnnecessaryBuilds()\'', done => {
|
||||
promise.then(result => {
|
||||
expect(result).toBe('Test');
|
||||
done();
|
||||
});
|
||||
|
||||
cleanerRemoveUnnecessaryBuildsSpy.and.returnValue(Promise.resolve('Test'));
|
||||
existingBuildsDeferred.resolve();
|
||||
openPrsDeferred.resolve();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
// Protected methods
|
||||
|
||||
describe('getExistingBuildNumbers()', () => {
|
||||
let fsReaddirSpy: jasmine.Spy;
|
||||
let readdirCb: (err: any, files?: string[]) => void;
|
||||
let promise: Promise<number[]>;
|
||||
|
||||
beforeEach(() => {
|
||||
fsReaddirSpy = spyOn(fs, 'readdir').and.callFake((_: string, cb: typeof readdirCb) => readdirCb = cb);
|
||||
promise = (cleaner as any).getExistingBuildNumbers();
|
||||
});
|
||||
|
||||
|
||||
it('should return a promise', () => {
|
||||
expect(promise).toEqual(jasmine.any(Promise));
|
||||
});
|
||||
|
||||
|
||||
it('should get the contents of the builds directory', () => {
|
||||
expect(fsReaddirSpy).toHaveBeenCalled();
|
||||
expect(fsReaddirSpy.calls.argsFor(0)[0]).toBe('/foo/bar');
|
||||
});
|
||||
|
||||
|
||||
it('should reject if an error occurs while getting the files', done => {
|
||||
promise.catch(err => {
|
||||
expect(err).toBe('Test');
|
||||
done();
|
||||
});
|
||||
|
||||
readdirCb('Test');
|
||||
});
|
||||
|
||||
|
||||
it('should resolve with the returned files (as numbers)', done => {
|
||||
promise.then(result => {
|
||||
expect(result).toEqual([12, 34, 56]);
|
||||
done();
|
||||
});
|
||||
|
||||
readdirCb(null, ['12', '34', '56']);
|
||||
});
|
||||
|
||||
|
||||
it('should ignore files with non-numeric (or zero) names', done => {
|
||||
promise.then(result => {
|
||||
expect(result).toEqual([12, 34, 56]);
|
||||
done();
|
||||
});
|
||||
|
||||
readdirCb(null, ['12', 'foo', '34', 'bar', '56', '000']);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
describe('getOpenPrNumbers()', () => {
|
||||
let prDeferred: {resolve: Function, reject: Function};
|
||||
let promise: Promise<number[]>;
|
||||
|
||||
beforeEach(() => {
|
||||
spyOn(GithubPullRequests.prototype, 'fetchAll').and.callFake(() => {
|
||||
return new Promise((resolve, reject) => prDeferred = {resolve, reject});
|
||||
});
|
||||
|
||||
promise = (cleaner as any).getOpenPrNumbers();
|
||||
});
|
||||
|
||||
|
||||
it('should return a promise', () => {
|
||||
expect(promise).toEqual(jasmine.any(Promise));
|
||||
});
|
||||
|
||||
|
||||
it('should fetch open PRs via \'GithubPullRequests\'', () => {
|
||||
expect(GithubPullRequests.prototype.fetchAll).toHaveBeenCalledWith('open');
|
||||
});
|
||||
|
||||
|
||||
it('should reject if an error occurs while fetching PRs', done => {
|
||||
promise.catch(err => {
|
||||
expect(err).toBe('Test');
|
||||
done();
|
||||
});
|
||||
|
||||
prDeferred.reject('Test');
|
||||
});
|
||||
|
||||
|
||||
it('should resolve with the numbers of the fetched PRs', done => {
|
||||
promise.then(prNumbers => {
|
||||
expect(prNumbers).toEqual([1, 2, 3]);
|
||||
done();
|
||||
});
|
||||
|
||||
prDeferred.resolve([{id: 0, number: 1}, {id: 1, number: 2}, {id: 2, number: 3}]);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
describe('removeDir()', () => {
|
||||
let shellChmodSpy: jasmine.Spy;
|
||||
let shellRmSpy: jasmine.Spy;
|
||||
|
||||
beforeEach(() => {
|
||||
shellChmodSpy = spyOn(shell, 'chmod');
|
||||
shellRmSpy = spyOn(shell, 'rm');
|
||||
});
|
||||
|
||||
|
||||
it('should remove the specified directory and its content', () => {
|
||||
(cleaner as any).removeDir('/foo/bar');
|
||||
expect(shellRmSpy).toHaveBeenCalledWith('-rf', '/foo/bar');
|
||||
});
|
||||
|
||||
|
||||
it('should make the directory and its content writable before removing', () => {
|
||||
shellRmSpy.and.callFake(() => expect(shellChmodSpy).toHaveBeenCalledWith('-R', 'a+w', '/foo/bar'));
|
||||
(cleaner as any).removeDir('/foo/bar');
|
||||
|
||||
expect(shellRmSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
|
||||
it('should catch errors and log them', () => {
|
||||
const consoleErrorSpy = spyOn(console, 'error');
|
||||
shellRmSpy.and.callFake(() => { throw 'Test'; });
|
||||
|
||||
(cleaner as any).removeDir('/foo/bar');
|
||||
|
||||
expect(consoleErrorSpy).toHaveBeenCalled();
|
||||
expect(consoleErrorSpy.calls.argsFor(0)[0]).toContain('Unable to remove \'/foo/bar\'');
|
||||
expect(consoleErrorSpy.calls.argsFor(0)[1]).toBe('Test');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
describe('removeUnnecessaryBuilds()', () => {
|
||||
let consoleLogSpy: jasmine.Spy;
|
||||
let cleanerRemoveDirSpy: jasmine.Spy;
|
||||
|
||||
beforeEach(() => {
|
||||
consoleLogSpy = spyOn(console, 'log');
|
||||
cleanerRemoveDirSpy = spyOn(cleaner as any, 'removeDir');
|
||||
});
|
||||
|
||||
|
||||
it('should log the number of existing builds, open PRs and builds to be removed', () => {
|
||||
(cleaner as any).removeUnnecessaryBuilds([1, 2, 3], [3, 4, 5, 6]);
|
||||
|
||||
expect(console.log).toHaveBeenCalledWith('Existing builds: 3');
|
||||
expect(console.log).toHaveBeenCalledWith('Open pull requests: 4');
|
||||
expect(console.log).toHaveBeenCalledWith('Removing 2 build(s): 1, 2');
|
||||
});
|
||||
|
||||
|
||||
it('should construct full paths to directories (by prepending \'buildsDir\')', () => {
|
||||
(cleaner as any).removeUnnecessaryBuilds([1, 2, 3], []);
|
||||
|
||||
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith('/foo/bar/1');
|
||||
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith('/foo/bar/2');
|
||||
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith('/foo/bar/3');
|
||||
});
|
||||
|
||||
|
||||
it('should remove the builds that do not correspond to open PRs', () => {
|
||||
(cleaner as any).removeUnnecessaryBuilds([1, 2, 3, 4], [2, 4]);
|
||||
expect(cleanerRemoveDirSpy).toHaveBeenCalledTimes(2);
|
||||
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith('/foo/bar/1');
|
||||
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith('/foo/bar/3');
|
||||
cleanerRemoveDirSpy.calls.reset();
|
||||
|
||||
(cleaner as any).removeUnnecessaryBuilds([1, 2, 3, 4], [1, 2, 3, 4]);
|
||||
expect(cleanerRemoveDirSpy).toHaveBeenCalledTimes(0);
|
||||
cleanerRemoveDirSpy.calls.reset();
|
||||
|
||||
(cleaner as any).removeUnnecessaryBuilds([1, 2, 3, 4], []);
|
||||
expect(cleanerRemoveDirSpy).toHaveBeenCalledTimes(4);
|
||||
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith('/foo/bar/1');
|
||||
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith('/foo/bar/2');
|
||||
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith('/foo/bar/3');
|
||||
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith('/foo/bar/4');
|
||||
cleanerRemoveDirSpy.calls.reset();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
@ -1,410 +0,0 @@
|
||||
// Imports
|
||||
import {EventEmitter} from 'events';
|
||||
import {ClientRequest, IncomingMessage} from 'http';
|
||||
import * as https from 'https';
|
||||
import {GithubApi} from '../../lib/common/github-api';
|
||||
|
||||
// Tests
|
||||
describe('GithubApi', () => {
|
||||
let api: GithubApi;
|
||||
|
||||
beforeEach(() => api = new GithubApi('12345'));
|
||||
|
||||
|
||||
describe('constructor()', () => {
|
||||
|
||||
it('should throw if \'githubToken\' is missing or empty', () => {
|
||||
expect(() => new GithubApi('')).toThrowError('Missing or empty required parameter \'githubToken\'!');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
describe('get()', () => {
|
||||
let apiBuildPathSpy: jasmine.Spy;
|
||||
let apiRequestSpy: jasmine.Spy;
|
||||
|
||||
beforeEach(() => {
|
||||
apiBuildPathSpy = spyOn(api as any, 'buildPath');
|
||||
apiRequestSpy = spyOn(api as any, 'request');
|
||||
});
|
||||
|
||||
|
||||
it('should call \'buildPath()\' with the pathname and params', () => {
|
||||
api.get('/foo', {bar: 'baz'});
|
||||
|
||||
expect(apiBuildPathSpy).toHaveBeenCalled();
|
||||
expect(apiBuildPathSpy.calls.argsFor(0)).toEqual(['/foo', {bar: 'baz'}]);
|
||||
});
|
||||
|
||||
|
||||
it('should call \'request()\' with the correct method', () => {
|
||||
api.get('/foo');
|
||||
|
||||
expect(apiRequestSpy).toHaveBeenCalled();
|
||||
expect(apiRequestSpy.calls.argsFor(0)[0]).toBe('get');
|
||||
});
|
||||
|
||||
|
||||
it('should call \'request()\' with the correct path', () => {
|
||||
apiBuildPathSpy.and.returnValue('/foo/bar');
|
||||
api.get('foo');
|
||||
|
||||
expect(apiRequestSpy).toHaveBeenCalled();
|
||||
expect(apiRequestSpy.calls.argsFor(0)[1]).toBe('/foo/bar');
|
||||
});
|
||||
|
||||
|
||||
it('should not pass data to \'request()\'', () => {
|
||||
(api.get as Function)('foo', {}, {});
|
||||
|
||||
expect(apiRequestSpy).toHaveBeenCalled();
|
||||
expect(apiRequestSpy.calls.argsFor(0)[2]).toBeUndefined();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
describe('post()', () => {
|
||||
let apiBuildPathSpy: jasmine.Spy;
|
||||
let apiRequestSpy: jasmine.Spy;
|
||||
|
||||
beforeEach(() => {
|
||||
apiBuildPathSpy = spyOn(api as any, 'buildPath');
|
||||
apiRequestSpy = spyOn(api as any, 'request');
|
||||
});
|
||||
|
||||
|
||||
it('should call \'buildPath()\' with the pathname and params', () => {
|
||||
api.post('/foo', {bar: 'baz'});
|
||||
|
||||
expect(apiBuildPathSpy).toHaveBeenCalled();
|
||||
expect(apiBuildPathSpy.calls.argsFor(0)).toEqual(['/foo', {bar: 'baz'}]);
|
||||
});
|
||||
|
||||
|
||||
it('should call \'request()\' with the correct method', () => {
|
||||
api.post('/foo');
|
||||
|
||||
expect(apiRequestSpy).toHaveBeenCalled();
|
||||
expect(apiRequestSpy.calls.argsFor(0)[0]).toBe('post');
|
||||
});
|
||||
|
||||
|
||||
it('should call \'request()\' with the correct path', () => {
|
||||
apiBuildPathSpy.and.returnValue('/foo/bar');
|
||||
api.post('/foo');
|
||||
|
||||
expect(apiRequestSpy).toHaveBeenCalled();
|
||||
expect(apiRequestSpy.calls.argsFor(0)[1]).toBe('/foo/bar');
|
||||
});
|
||||
|
||||
|
||||
it('should pass the data to \'request()\'', () => {
|
||||
api.post('/foo', {}, {bar: 'baz'});
|
||||
|
||||
expect(apiRequestSpy).toHaveBeenCalled();
|
||||
expect(apiRequestSpy.calls.argsFor(0)[2]).toEqual({bar: 'baz'});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
// Protected methods
|
||||
|
||||
describe('buildPath()', () => {
|
||||
|
||||
it('should return the pathname if no params', () => {
|
||||
expect((api as any).buildPath('/foo')).toBe('/foo');
|
||||
expect((api as any).buildPath('/foo', undefined)).toBe('/foo');
|
||||
expect((api as any).buildPath('/foo', null)).toBe('/foo');
|
||||
});
|
||||
|
||||
|
||||
it('should append the params to the pathname', () => {
|
||||
expect((api as any).buildPath('/foo', {bar: 'baz'})).toBe('/foo?bar=baz');
|
||||
});
|
||||
|
||||
|
||||
it('should join the params with \'&\'', () => {
|
||||
expect((api as any).buildPath('/foo', {bar: 1, baz: 2})).toBe('/foo?bar=1&baz=2');
|
||||
});
|
||||
|
||||
|
||||
it('should ignore undefined/null params', () => {
|
||||
expect((api as any).buildPath('/foo', {bar: undefined, baz: null})).toBe('/foo');
|
||||
});
|
||||
|
||||
|
||||
it('should encode param values as URI components', () => {
|
||||
expect((api as any).buildPath('/foo', {bar: 'b a&z'})).toBe('/foo?bar=b%20a%26z');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
describe('getPaginated()', () => {
|
||||
let deferreds: {resolve: Function, reject: Function}[];
|
||||
|
||||
beforeEach(() => {
|
||||
deferreds = [];
|
||||
spyOn(api, 'get').and.callFake(() => new Promise((resolve, reject) => deferreds.push({resolve, reject})));
|
||||
});
|
||||
|
||||
|
||||
it('should return a promise', () => {
|
||||
expect((api as any).getPaginated()).toEqual(jasmine.any(Promise));
|
||||
});
|
||||
|
||||
|
||||
it('should call \'get()\' with the correct pathname and params', () => {
|
||||
(api as any).getPaginated('/foo/bar');
|
||||
(api as any).getPaginated('/foo/bar', {baz: 'qux'});
|
||||
|
||||
expect(api.get).toHaveBeenCalledWith('/foo/bar', {page: 0, per_page: 100});
|
||||
expect(api.get).toHaveBeenCalledWith('/foo/bar', {baz: 'qux', page: 0, per_page: 100});
|
||||
});
|
||||
|
||||
|
||||
it('should reject if the request fails', done => {
|
||||
(api as any).getPaginated('/foo/bar').catch((err: any) => {
|
||||
expect(err).toBe('Test');
|
||||
done();
|
||||
});
|
||||
|
||||
deferreds[0].reject('Test');
|
||||
});
|
||||
|
||||
|
||||
it('should resolve with the returned items', done => {
|
||||
const items = [{id: 1}, {id: 2}];
|
||||
|
||||
(api as any).getPaginated('/foo/bar').then((data: any) => {
|
||||
expect(data).toEqual(items);
|
||||
done();
|
||||
});
|
||||
|
||||
deferreds[0].resolve(items);
|
||||
});
|
||||
|
||||
|
||||
it('should iteratively call \'get()\' to fetch all items', done => {
|
||||
// Create an array or 250 objects.
|
||||
const allItems = '.'.repeat(250).split('').map((_, i) => ({id: i}));
|
||||
const apiGetSpy = api.get as jasmine.Spy;
|
||||
|
||||
(api as any).getPaginated('/foo/bar', {baz: 'qux'}).then((data: any) => {
|
||||
const paramsForPage = (page: number) => ({baz: 'qux', page, per_page: 100});
|
||||
|
||||
expect(apiGetSpy).toHaveBeenCalledTimes(3);
|
||||
expect(apiGetSpy.calls.argsFor(0)).toEqual(['/foo/bar', paramsForPage(0)]);
|
||||
expect(apiGetSpy.calls.argsFor(1)).toEqual(['/foo/bar', paramsForPage(1)]);
|
||||
expect(apiGetSpy.calls.argsFor(2)).toEqual(['/foo/bar', paramsForPage(2)]);
|
||||
|
||||
expect(data).toEqual(allItems);
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
deferreds[0].resolve(allItems.slice(0, 100));
|
||||
setTimeout(() => {
|
||||
deferreds[1].resolve(allItems.slice(100, 200));
|
||||
setTimeout(() => {
|
||||
deferreds[2].resolve(allItems.slice(200));
|
||||
}, 0);
|
||||
}, 0);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
describe('request()', () => {
|
||||
let httpsRequestSpy: jasmine.Spy;
|
||||
let latestRequest: ClientRequest;
|
||||
|
||||
beforeEach(() => {
|
||||
const originalRequest = https.request;
|
||||
|
||||
httpsRequestSpy = spyOn(https, 'request').and.callFake((...args: any[]) => {
|
||||
latestRequest = originalRequest.apply(https, args);
|
||||
|
||||
spyOn(latestRequest, 'on').and.callThrough();
|
||||
spyOn(latestRequest, 'end');
|
||||
|
||||
return latestRequest;
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should return a promise', () => {
|
||||
expect((api as any).request()).toEqual(jasmine.any(Promise));
|
||||
});
|
||||
|
||||
|
||||
it('should call \'https.request()\' with the correct options', () => {
|
||||
(api as any).request('method', 'path');
|
||||
|
||||
expect(httpsRequestSpy).toHaveBeenCalled();
|
||||
expect(httpsRequestSpy.calls.argsFor(0)[0]).toEqual(jasmine.objectContaining({
|
||||
headers: jasmine.objectContaining({
|
||||
'User-Agent': `Node/${process.versions.node}`,
|
||||
}),
|
||||
host: 'api.github.com',
|
||||
method: 'method',
|
||||
path: 'path',
|
||||
}));
|
||||
});
|
||||
|
||||
|
||||
it('should call specify an \'Authorization\' header if \'githubToken\' is present', () => {
|
||||
(api as any).request('method', 'path');
|
||||
|
||||
expect(httpsRequestSpy).toHaveBeenCalled();
|
||||
expect(httpsRequestSpy.calls.argsFor(0)[0].headers).toEqual(jasmine.objectContaining({
|
||||
Authorization: 'token 12345',
|
||||
}));
|
||||
});
|
||||
|
||||
|
||||
it('should reject on request error', done => {
|
||||
(api as any).request('method', 'path').catch((err: any) => {
|
||||
expect(err).toBe('Test');
|
||||
done();
|
||||
});
|
||||
|
||||
latestRequest.emit('error', 'Test');
|
||||
});
|
||||
|
||||
|
||||
it('should send the request (i.e. call \'end()\')', () => {
|
||||
(api as any).request('method', 'path');
|
||||
expect(latestRequest.end).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
|
||||
it('should \'JSON.stringify\' and send the data along with the request', () => {
|
||||
(api as any).request('method', 'path');
|
||||
expect(latestRequest.end).toHaveBeenCalledWith(null);
|
||||
|
||||
(api as any).request('method', 'path', {key: 'value'});
|
||||
expect(latestRequest.end).toHaveBeenCalledWith('{"key":"value"}');
|
||||
});
|
||||
|
||||
|
||||
describe('onResponse', () => {
|
||||
let promise: Promise<void>;
|
||||
let respond: (statusCode: number) => IncomingMessage;
|
||||
|
||||
beforeEach(() => {
|
||||
promise = (api as any).request('method', 'path');
|
||||
|
||||
respond = (statusCode: number) => {
|
||||
const mockResponse = new EventEmitter() as IncomingMessage;
|
||||
mockResponse.statusCode = statusCode;
|
||||
|
||||
const onResponse = httpsRequestSpy.calls.argsFor(0)[1];
|
||||
onResponse(mockResponse);
|
||||
|
||||
return mockResponse;
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
it('should reject on response error', done => {
|
||||
promise.catch(err => {
|
||||
expect(err).toBe('Test');
|
||||
done();
|
||||
});
|
||||
|
||||
const res = respond(200);
|
||||
res.emit('error', 'Test');
|
||||
});
|
||||
|
||||
|
||||
it('should reject if returned statusCode is <200', done => {
|
||||
promise.catch(err => {
|
||||
expect(err).toContain('failed');
|
||||
expect(err).toContain('status: 199');
|
||||
done();
|
||||
});
|
||||
|
||||
const res = respond(199);
|
||||
res.emit('end');
|
||||
});
|
||||
|
||||
|
||||
it('should reject if returned statusCode is >=400', done => {
|
||||
promise.catch(err => {
|
||||
expect(err).toContain('failed');
|
||||
expect(err).toContain('status: 400');
|
||||
done();
|
||||
});
|
||||
|
||||
const res = respond(400);
|
||||
res.emit('end');
|
||||
});
|
||||
|
||||
|
||||
it('should include the response text in the rejection message', done => {
|
||||
promise.catch(err => {
|
||||
expect(err).toContain('Test');
|
||||
done();
|
||||
});
|
||||
|
||||
const res = respond(500);
|
||||
res.emit('data', 'Test');
|
||||
res.emit('end');
|
||||
});
|
||||
|
||||
|
||||
it('should resolve if returned statusCode is <=200 <400', done => {
|
||||
promise.then(done);
|
||||
|
||||
const res = respond(200);
|
||||
res.emit('data', '{}');
|
||||
res.emit('end');
|
||||
});
|
||||
|
||||
|
||||
it('should resolve with the response text \'JSON.parsed\'', done => {
|
||||
promise.then(data => {
|
||||
expect(data).toEqual({foo: 'bar'});
|
||||
done();
|
||||
});
|
||||
|
||||
const res = respond(300);
|
||||
res.emit('data', '{"foo":"bar"}');
|
||||
res.emit('end');
|
||||
});
|
||||
|
||||
|
||||
it('should collect and concatenate the whole response text', done => {
|
||||
promise.then(data => {
|
||||
expect(data).toEqual({foo: 'bar', baz: 'qux'});
|
||||
done();
|
||||
});
|
||||
|
||||
const res = respond(300);
|
||||
res.emit('data', '{"foo":');
|
||||
res.emit('data', '"bar","baz"');
|
||||
res.emit('data', ':"qux"}');
|
||||
res.emit('end');
|
||||
});
|
||||
|
||||
|
||||
it('should reject if the response text is malformed JSON', done => {
|
||||
promise.catch(err => {
|
||||
expect(err).toEqual(jasmine.any(SyntaxError));
|
||||
done();
|
||||
});
|
||||
|
||||
const res = respond(300);
|
||||
res.emit('data', '}');
|
||||
res.emit('end');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
@ -1,117 +0,0 @@
|
||||
// Imports
|
||||
import {GithubPullRequests} from '../../lib/common/github-pull-requests';
|
||||
|
||||
// Tests
|
||||
describe('GithubPullRequests', () => {
|
||||
|
||||
describe('constructor()', () => {
|
||||
|
||||
it('should throw if \'githubToken\' is missing or empty', () => {
|
||||
expect(() => new GithubPullRequests('', 'foo/bar')).
|
||||
toThrowError('Missing or empty required parameter \'githubToken\'!');
|
||||
});
|
||||
|
||||
|
||||
it('should throw if \'repoSlug\' is missing or empty', () => {
|
||||
expect(() => new GithubPullRequests('12345', '')).
|
||||
toThrowError('Missing or empty required parameter \'repoSlug\'!');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
describe('addComment()', () => {
|
||||
let prs: GithubPullRequests;
|
||||
let deferred: {resolve: Function, reject: Function};
|
||||
|
||||
beforeEach(() => {
|
||||
prs = new GithubPullRequests('12345', 'foo/bar');
|
||||
|
||||
spyOn(prs, 'post').and.callFake(() => new Promise((resolve, reject) => deferred = {resolve, reject}));
|
||||
});
|
||||
|
||||
|
||||
it('should return a promise', () => {
|
||||
expect(prs.addComment(42, 'body')).toEqual(jasmine.any(Promise));
|
||||
});
|
||||
|
||||
|
||||
it('should throw if the PR number is invalid', () => {
|
||||
expect(() => prs.addComment(-1337, 'body')).toThrowError(`Invalid PR number: -1337`);
|
||||
expect(() => prs.addComment(NaN, 'body')).toThrowError(`Invalid PR number: NaN`);
|
||||
});
|
||||
|
||||
|
||||
it('should throw if the comment body is invalid or empty', () => {
|
||||
expect(() => prs.addComment(42, '')).toThrowError(`Invalid or empty comment body: `);
|
||||
});
|
||||
|
||||
|
||||
it('should call \'post()\' with the correct pathname, params and data', () => {
|
||||
prs.addComment(42, 'body');
|
||||
|
||||
expect(prs.post).toHaveBeenCalledWith('/repos/foo/bar/issues/42/comments', null, {body: 'body'});
|
||||
});
|
||||
|
||||
|
||||
it('should reject if the request fails', done => {
|
||||
prs.addComment(42, 'body').catch(err => {
|
||||
expect(err).toBe('Test');
|
||||
done();
|
||||
});
|
||||
|
||||
deferred.reject('Test');
|
||||
});
|
||||
|
||||
|
||||
it('should resolve with the returned response', done => {
|
||||
prs.addComment(42, 'body').then(data => {
|
||||
expect(data).toEqual('Test');
|
||||
done();
|
||||
});
|
||||
|
||||
deferred.resolve('Test');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
describe('fetchAll()', () => {
|
||||
let prs: GithubPullRequests;
|
||||
let prsGetPaginatedSpy: jasmine.Spy;
|
||||
|
||||
beforeEach(() => {
|
||||
prs = new GithubPullRequests('12345', 'foo/bar');
|
||||
prsGetPaginatedSpy = spyOn(prs as any, 'getPaginated');
|
||||
spyOn(console, 'log');
|
||||
});
|
||||
|
||||
|
||||
it('should call \'getPaginated()\' with the correct pathname and params', () => {
|
||||
const expectedPathname = '/repos/foo/bar/pulls';
|
||||
|
||||
prs.fetchAll('all');
|
||||
prs.fetchAll('closed');
|
||||
prs.fetchAll('open');
|
||||
|
||||
expect(prsGetPaginatedSpy).toHaveBeenCalledTimes(3);
|
||||
expect(prsGetPaginatedSpy.calls.argsFor(0)).toEqual([expectedPathname, {state: 'all'}]);
|
||||
expect(prsGetPaginatedSpy.calls.argsFor(1)).toEqual([expectedPathname, {state: 'closed'}]);
|
||||
expect(prsGetPaginatedSpy.calls.argsFor(2)).toEqual([expectedPathname, {state: 'open'}]);
|
||||
});
|
||||
|
||||
|
||||
it('should default to \'all\' if no state is specified', () => {
|
||||
prs.fetchAll();
|
||||
expect(prsGetPaginatedSpy).toHaveBeenCalledWith('/repos/foo/bar/pulls', {state: 'all'});
|
||||
});
|
||||
|
||||
|
||||
it('should forward the value returned by \'getPaginated()\'', () => {
|
||||
prsGetPaginatedSpy.and.returnValue('Test');
|
||||
expect(prs.fetchAll()).toBe('Test');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
@ -1,232 +0,0 @@
|
||||
// Imports
|
||||
import {GithubTeams} from '../../lib/common/github-teams';
|
||||
|
||||
// Tests
|
||||
describe('GithubTeams', () => {
|
||||
|
||||
describe('constructor()', () => {
|
||||
|
||||
it('should throw if \'githubToken\' is missing or empty', () => {
|
||||
expect(() => new GithubTeams('', 'org')).
|
||||
toThrowError('Missing or empty required parameter \'githubToken\'!');
|
||||
});
|
||||
|
||||
|
||||
it('should throw if \'organization\' is missing or empty', () => {
|
||||
expect(() => new GithubTeams('12345', '')).
|
||||
toThrowError('Missing or empty required parameter \'organization\'!');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
describe('fetchAll()', () => {
|
||||
let teams: GithubTeams;
|
||||
let teamsGetPaginatedSpy: jasmine.Spy;
|
||||
|
||||
beforeEach(() => {
|
||||
teams = new GithubTeams('12345', 'foo');
|
||||
teamsGetPaginatedSpy = spyOn(teams as any, 'getPaginated');
|
||||
});
|
||||
|
||||
|
||||
it('should call \'getPaginated()\' with the correct pathname and params', () => {
|
||||
teams.fetchAll();
|
||||
expect(teamsGetPaginatedSpy).toHaveBeenCalledWith('/orgs/foo/teams');
|
||||
});
|
||||
|
||||
|
||||
it('should forward the value returned by \'getPaginated()\'', () => {
|
||||
teamsGetPaginatedSpy.and.returnValue('Test');
|
||||
expect(teams.fetchAll()).toBe('Test');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
describe('isMemberById()', () => {
|
||||
let teams: GithubTeams;
|
||||
let teamsGetSpy: jasmine.Spy;
|
||||
|
||||
beforeEach(() => {
|
||||
teams = new GithubTeams('12345', 'foo');
|
||||
teamsGetSpy = spyOn(teams, 'get');
|
||||
});
|
||||
|
||||
|
||||
it('should return a promise', () => {
|
||||
expect(teams.isMemberById('user', [1])).toEqual(jasmine.any(Promise));
|
||||
});
|
||||
|
||||
|
||||
it('should resolve with false if called with an empty array', done => {
|
||||
teams.isMemberById('user', []).then(isMember => {
|
||||
expect(isMember).toBe(false);
|
||||
expect(teamsGetSpy).not.toHaveBeenCalled();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should call \'get()\' with the correct pathname', done => {
|
||||
teamsGetSpy.and.returnValue(Promise.resolve(null));
|
||||
teams.isMemberById('user', [1]).then(() => {
|
||||
expect(teamsGetSpy).toHaveBeenCalledWith('/teams/1/memberships/user');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should resolve with false if \'get()\' rejects', done => {
|
||||
teamsGetSpy.and.returnValue(Promise.reject(null));
|
||||
teams.isMemberById('user', [1]).then(isMember => {
|
||||
expect(isMember).toBe(false);
|
||||
expect(teamsGetSpy).toHaveBeenCalled();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should resolve with false if the membership is not active', done => {
|
||||
teamsGetSpy.and.returnValue(Promise.resolve({state: 'pending'}));
|
||||
teams.isMemberById('user', [1]).then(isMember => {
|
||||
expect(isMember).toBe(false);
|
||||
expect(teamsGetSpy).toHaveBeenCalled();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should resolve with true if the membership is active', done => {
|
||||
teamsGetSpy.and.returnValue(Promise.resolve({state: 'active'}));
|
||||
teams.isMemberById('user', [1]).then(isMember => {
|
||||
expect(isMember).toBe(true);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should sequentially call \'get()\' until an active membership is found', done => {
|
||||
const trainedResponses: {[pathname: string]: Promise<{state: string}>} = {
|
||||
'/teams/1/memberships/user': Promise.resolve({state: 'pending'}),
|
||||
'/teams/2/memberships/user': Promise.reject(null),
|
||||
'/teams/3/memberships/user': Promise.resolve({state: 'active'}),
|
||||
};
|
||||
teamsGetSpy.and.callFake((pathname: string) => trainedResponses[pathname]);
|
||||
|
||||
teams.isMemberById('user', [1, 2, 3, 4]).then(isMember => {
|
||||
expect(isMember).toBe(true);
|
||||
|
||||
expect(teamsGetSpy).toHaveBeenCalledTimes(3);
|
||||
expect(teamsGetSpy.calls.argsFor(0)[0]).toBe('/teams/1/memberships/user');
|
||||
expect(teamsGetSpy.calls.argsFor(1)[0]).toBe('/teams/2/memberships/user');
|
||||
expect(teamsGetSpy.calls.argsFor(2)[0]).toBe('/teams/3/memberships/user');
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should resolve with false if no active membership is found', done => {
|
||||
const trainedResponses: {[pathname: string]: Promise<{state: string}>} = {
|
||||
'/teams/1/memberships/user': Promise.resolve({state: 'pending'}),
|
||||
'/teams/2/memberships/user': Promise.reject(null),
|
||||
'/teams/3/memberships/user': Promise.resolve({state: 'not active'}),
|
||||
'/teams/4/memberships/user': Promise.reject(null),
|
||||
};
|
||||
teamsGetSpy.and.callFake((pathname: string) => trainedResponses[pathname]);
|
||||
|
||||
teams.isMemberById('user', [1, 2, 3, 4]).then(isMember => {
|
||||
expect(isMember).toBe(false);
|
||||
|
||||
expect(teamsGetSpy).toHaveBeenCalledTimes(4);
|
||||
expect(teamsGetSpy.calls.argsFor(0)[0]).toBe('/teams/1/memberships/user');
|
||||
expect(teamsGetSpy.calls.argsFor(1)[0]).toBe('/teams/2/memberships/user');
|
||||
expect(teamsGetSpy.calls.argsFor(2)[0]).toBe('/teams/3/memberships/user');
|
||||
expect(teamsGetSpy.calls.argsFor(3)[0]).toBe('/teams/4/memberships/user');
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
describe('isMemberBySlug()', () => {
|
||||
let teams: GithubTeams;
|
||||
let teamsFetchAllSpy: jasmine.Spy;
|
||||
let teamsIsMemberByIdSpy: jasmine.Spy;
|
||||
|
||||
beforeEach(() => {
|
||||
teams = new GithubTeams('12345', 'foo');
|
||||
|
||||
const mockResponse = Promise.resolve([{id: 1, slug: 'team1'}, {id: 2, slug: 'team2'}]);
|
||||
teamsFetchAllSpy = spyOn(teams, 'fetchAll').and.returnValue(mockResponse);
|
||||
teamsIsMemberByIdSpy = spyOn(teams, 'isMemberById');
|
||||
});
|
||||
|
||||
|
||||
it('should return a promise', () => {
|
||||
expect(teams.isMemberBySlug('user', ['team-slug'])).toEqual(jasmine.any(Promise));
|
||||
});
|
||||
|
||||
|
||||
it('should call \'fetchAll()\'', () => {
|
||||
teams.isMemberBySlug('user', ['team-slug']);
|
||||
expect(teamsFetchAllSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
|
||||
it('should resolve with false if \'fetchAll()\' rejects', done => {
|
||||
teamsFetchAllSpy.and.returnValue(Promise.reject(null));
|
||||
teams.isMemberBySlug('user', ['team-slug']).then(isMember => {
|
||||
expect(isMember).toBe(false);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should call \'isMemberById()\' with the correct params if no team is found', done => {
|
||||
teams.isMemberBySlug('user', ['no-match']).then(() => {
|
||||
expect(teamsIsMemberByIdSpy).toHaveBeenCalledWith('user', []);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should call \'isMemberById()\' with the correct params if teams are found', done => {
|
||||
const spy = teamsIsMemberByIdSpy;
|
||||
|
||||
Promise.all([
|
||||
teams.isMemberBySlug('user', ['team1']).then(() => expect(spy).toHaveBeenCalledWith('user', [1])),
|
||||
teams.isMemberBySlug('user', ['team2']).then(() => expect(spy).toHaveBeenCalledWith('user', [2])),
|
||||
teams.isMemberBySlug('user', ['team1', 'team2']).then(() => expect(spy).toHaveBeenCalledWith('user', [1, 2])),
|
||||
]).then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should resolve with false if \'isMemberById()\' rejects', done => {
|
||||
teamsIsMemberByIdSpy.and.returnValue(Promise.reject(null));
|
||||
teams.isMemberBySlug('user', ['team1']).then(isMember => {
|
||||
expect(isMember).toBe(false);
|
||||
expect(teamsIsMemberByIdSpy).toHaveBeenCalled();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should resolve with the value \'isMemberById()\' resolves with', done => {
|
||||
teamsIsMemberByIdSpy.and.returnValues(Promise.resolve(false), Promise.resolve(true));
|
||||
|
||||
Promise.all([
|
||||
teams.isMemberBySlug('user', ['team1']).then(isMember => expect(isMember).toBe(false)),
|
||||
teams.isMemberBySlug('user', ['team1']).then(isMember => expect(isMember).toBe(true)),
|
||||
]).then(() => {
|
||||
expect(teamsIsMemberByIdSpy).toHaveBeenCalledTimes(2);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
@ -1,81 +0,0 @@
|
||||
// Imports
|
||||
import {assertNotMissingOrEmpty, getEnvVar} from '../../lib/common/utils';
|
||||
|
||||
// Tests
|
||||
describe('utils', () => {
|
||||
|
||||
describe('assertNotMissingOrEmpty()', () => {
|
||||
|
||||
it('should throw if passed an empty value', () => {
|
||||
expect(() => assertNotMissingOrEmpty('foo', undefined)).
|
||||
toThrowError('Missing or empty required parameter \'foo\'!');
|
||||
expect(() => assertNotMissingOrEmpty('bar', null)).toThrowError('Missing or empty required parameter \'bar\'!');
|
||||
expect(() => assertNotMissingOrEmpty('baz', '')).toThrowError('Missing or empty required parameter \'baz\'!');
|
||||
});
|
||||
|
||||
|
||||
it('should not throw if passed a non-empty value', () => {
|
||||
expect(() => assertNotMissingOrEmpty('foo', ' ')).not.toThrow();
|
||||
expect(() => assertNotMissingOrEmpty('bar', 'bar')).not.toThrow();
|
||||
expect(() => assertNotMissingOrEmpty('baz', 'b a z')).not.toThrow();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
describe('getEnvVar()', () => {
|
||||
const emptyVar = '$$test_utils_getEnvVar_empty$$';
|
||||
const nonEmptyVar = '$$test_utils_getEnvVar_nonEmpty$$';
|
||||
const undefinedVar = '$$test_utils_getEnvVar_undefined$$';
|
||||
|
||||
beforeEach(() => {
|
||||
process.env[emptyVar] = '';
|
||||
process.env[nonEmptyVar] = 'foo';
|
||||
});
|
||||
afterEach(() => {
|
||||
delete process.env[emptyVar];
|
||||
delete process.env[nonEmptyVar];
|
||||
});
|
||||
|
||||
|
||||
it('should return an environment variable', () => {
|
||||
expect(getEnvVar(nonEmptyVar)).toBe('foo');
|
||||
});
|
||||
|
||||
|
||||
it('should exit with an error if the environment variable is not defined', () => {
|
||||
const consoleErrorSpy = spyOn(console, 'error');
|
||||
const processExitSpy = spyOn(process, 'exit');
|
||||
|
||||
getEnvVar(undefinedVar);
|
||||
|
||||
expect(consoleErrorSpy).toHaveBeenCalled();
|
||||
expect(consoleErrorSpy.calls.argsFor(0)[0]).toContain(undefinedVar);
|
||||
expect(processExitSpy).toHaveBeenCalledWith(1);
|
||||
});
|
||||
|
||||
|
||||
it('should exit with an error if the environment variable is empty', () => {
|
||||
const consoleErrorSpy = spyOn(console, 'error');
|
||||
const processExitSpy = spyOn(process, 'exit');
|
||||
|
||||
getEnvVar(emptyVar);
|
||||
|
||||
expect(consoleErrorSpy).toHaveBeenCalled();
|
||||
expect(consoleErrorSpy.calls.argsFor(0)[0]).toContain(emptyVar);
|
||||
expect(processExitSpy).toHaveBeenCalledWith(1);
|
||||
});
|
||||
|
||||
|
||||
it('should return an empty string if an undefined variable is optional', () => {
|
||||
expect(getEnvVar(undefinedVar, true)).toBe('');
|
||||
});
|
||||
|
||||
|
||||
it('should return an empty string if an empty variable is optional', () => {
|
||||
expect(getEnvVar(emptyVar, true)).toBe('');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
@ -1,6 +0,0 @@
|
||||
declare namespace jasmine {
|
||||
export interface DoneFn extends Function {
|
||||
(): void;
|
||||
fail: (message: Error | string) => void;
|
||||
}
|
||||
}
|
@ -1,7 +0,0 @@
|
||||
// Imports
|
||||
import {runTests} from '../lib/common/run-tests';
|
||||
|
||||
// Run
|
||||
const specFiles = [`${__dirname}/**/*.spec.js`];
|
||||
const helpers = [`${__dirname}/helpers.js`];
|
||||
runTests(specFiles, helpers);
|
@ -1,320 +0,0 @@
|
||||
// Imports
|
||||
import * as cp from 'child_process';
|
||||
import {EventEmitter} from 'events';
|
||||
import * as fs from 'fs';
|
||||
import * as shell from 'shelljs';
|
||||
import {BuildCreator} from '../../lib/upload-server/build-creator';
|
||||
import {CreatedBuildEvent} from '../../lib/upload-server/build-events';
|
||||
import {UploadError} from '../../lib/upload-server/upload-error';
|
||||
import {expectToBeUploadError} from './helpers';
|
||||
|
||||
// Tests
|
||||
describe('BuildCreator', () => {
|
||||
const pr = '9';
|
||||
const sha = '9'.repeat(40);
|
||||
const archive = 'snapshot.tar.gz';
|
||||
const buildsDir = 'builds/dir';
|
||||
const prDir = `${buildsDir}/${pr}`;
|
||||
const shaDir = `${prDir}/${sha}`;
|
||||
let bc: BuildCreator;
|
||||
|
||||
beforeEach(() => bc = new BuildCreator(buildsDir));
|
||||
|
||||
|
||||
describe('constructor()', () => {
|
||||
|
||||
it('should throw if \'buildsDir\' is missing or empty', () => {
|
||||
expect(() => new BuildCreator('')).toThrowError('Missing or empty required parameter \'buildsDir\'!');
|
||||
});
|
||||
|
||||
|
||||
it('should extend EventEmitter', () => {
|
||||
expect(bc).toEqual(jasmine.any(BuildCreator));
|
||||
expect(bc).toEqual(jasmine.any(EventEmitter));
|
||||
|
||||
expect(Object.getPrototypeOf(bc)).toBe(BuildCreator.prototype);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
describe('create()', () => {
|
||||
let bcEmitSpy: jasmine.Spy;
|
||||
let bcExistsSpy: jasmine.Spy;
|
||||
let bcExtractArchiveSpy: jasmine.Spy;
|
||||
let shellMkdirSpy: jasmine.Spy;
|
||||
let shellRmSpy: jasmine.Spy;
|
||||
|
||||
beforeEach(() => {
|
||||
bcEmitSpy = spyOn(bc, 'emit');
|
||||
bcExistsSpy = spyOn(bc as any, 'exists');
|
||||
bcExtractArchiveSpy = spyOn(bc as any, 'extractArchive');
|
||||
shellMkdirSpy = spyOn(shell, 'mkdir');
|
||||
shellRmSpy = spyOn(shell, 'rm');
|
||||
});
|
||||
|
||||
|
||||
it('should return a promise', done => {
|
||||
const promise = bc.create(pr, sha, archive);
|
||||
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));
|
||||
});
|
||||
|
||||
|
||||
it('should throw if the build does already exist', done => {
|
||||
bcExistsSpy.and.returnValue(true);
|
||||
bc.create(pr, sha, archive).catch(err => {
|
||||
expectToBeUploadError(err, 403, `Request to overwrite existing directory: ${shaDir}`);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
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)).
|
||||
then(done);
|
||||
});
|
||||
|
||||
|
||||
describe('on error', () => {
|
||||
|
||||
it('should abort and skip further operations if it fails to create the directories', done => {
|
||||
shellMkdirSpy.and.throwError('');
|
||||
bc.create(pr, sha, archive).catch(() => {
|
||||
expect(shellMkdirSpy).toHaveBeenCalled();
|
||||
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).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).catch(() => {
|
||||
expect(shellRmSpy).toHaveBeenCalledWith('-rf', prDir);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should delete the SHA directory (for existing PR)', done => {
|
||||
bcExistsSpy.and.callFake((path: string) => path !== shaDir);
|
||||
bcExtractArchiveSpy.and.throwError('');
|
||||
|
||||
bc.create(pr, sha, archive).catch(() => {
|
||||
expect(shellRmSpy).toHaveBeenCalledWith('-rf', shaDir);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should reject with an UploadError', done => {
|
||||
shellMkdirSpy.and.callFake(() => {throw 'Test'; });
|
||||
bc.create(pr, sha, archive).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).catch(err => {
|
||||
expectToBeUploadError(err, 543, 'Test');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
// Protected methods
|
||||
|
||||
describe('exists()', () => {
|
||||
let fsAccessSpy: jasmine.Spy;
|
||||
let fsAccessCbs: Function[];
|
||||
|
||||
beforeEach(() => {
|
||||
fsAccessCbs = [];
|
||||
fsAccessSpy = spyOn(fs, 'access').and.callFake((_: string, cb: Function) => fsAccessCbs.push(cb));
|
||||
});
|
||||
|
||||
|
||||
it('should return a promise', () => {
|
||||
expect((bc as any).exists('foo')).toEqual(jasmine.any(Promise));
|
||||
});
|
||||
|
||||
|
||||
it('should call \'fs.access()\' with the specified argument', () => {
|
||||
(bc as any).exists('foo');
|
||||
expect(fs.access).toHaveBeenCalledWith('foo', jasmine.any(Function));
|
||||
});
|
||||
|
||||
|
||||
it('should resolve with \'true\' if \'fs.access()\' succeeds', done => {
|
||||
Promise.
|
||||
all([(bc as any).exists('foo'), (bc as any).exists('bar')]).
|
||||
then(results => expect(results).toEqual([true, true])).
|
||||
then(done);
|
||||
|
||||
fsAccessCbs[0]();
|
||||
fsAccessCbs[1](null);
|
||||
});
|
||||
|
||||
|
||||
it('should resolve with \'false\' if \'fs.access()\' errors', done => {
|
||||
Promise.
|
||||
all([(bc as any).exists('foo'), (bc as any).exists('bar')]).
|
||||
then(results => expect(results).toEqual([false, false])).
|
||||
then(done);
|
||||
|
||||
fsAccessCbs[0]('Error');
|
||||
fsAccessCbs[1](new Error());
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
describe('extractArchive()', () => {
|
||||
let consoleWarnSpy: jasmine.Spy;
|
||||
let shellChmodSpy: jasmine.Spy;
|
||||
let shellRmSpy: jasmine.Spy;
|
||||
let cpExecSpy: jasmine.Spy;
|
||||
let cpExecCbs: Function[];
|
||||
|
||||
beforeEach(() => {
|
||||
cpExecCbs = [];
|
||||
|
||||
consoleWarnSpy = spyOn(console, 'warn');
|
||||
shellChmodSpy = spyOn(shell, 'chmod');
|
||||
shellRmSpy = spyOn(shell, 'rm');
|
||||
cpExecSpy = spyOn(cp, 'exec').and.callFake((_: string, cb: Function) => cpExecCbs.push(cb));
|
||||
});
|
||||
|
||||
|
||||
it('should return a promise', () => {
|
||||
expect((bc as any).extractArchive('foo', 'bar')).toEqual(jasmine.any(Promise));
|
||||
});
|
||||
|
||||
|
||||
it('should "gunzip" and "untar" the input file into the output directory', () => {
|
||||
const cmd = 'tar --extract --gzip --directory "output/dir" --file "input/file"';
|
||||
|
||||
(bc as any).extractArchive('input/file', 'output/dir');
|
||||
expect(cpExecSpy).toHaveBeenCalledWith(cmd, jasmine.any(Function));
|
||||
});
|
||||
|
||||
|
||||
it('should log (as a warning) any stderr output if extracting succeeded', done => {
|
||||
(bc as any).extractArchive('foo', 'bar').
|
||||
then(() => expect(consoleWarnSpy).toHaveBeenCalledWith('This is stderr')).
|
||||
then(done);
|
||||
|
||||
cpExecCbs[0](null, 'This is stdout', 'This is stderr');
|
||||
});
|
||||
|
||||
|
||||
it('should make the build directory non-writable', done => {
|
||||
(bc as any).extractArchive('foo', 'bar').
|
||||
then(() => expect(shellChmodSpy).toHaveBeenCalledWith('-R', 'a-w', 'bar')).
|
||||
then(done);
|
||||
|
||||
cpExecCbs[0]();
|
||||
});
|
||||
|
||||
|
||||
it('should delete the uploaded file on success', done => {
|
||||
(bc as any).extractArchive('input/file', 'output/dir').
|
||||
then(() => expect(shellRmSpy).toHaveBeenCalledWith('-f', 'input/file')).
|
||||
then(done);
|
||||
|
||||
cpExecCbs[0]();
|
||||
});
|
||||
|
||||
|
||||
describe('on error', () => {
|
||||
|
||||
it('should abort and skip further operations if it fails to extract the archive', done => {
|
||||
(bc as any).extractArchive('foo', 'bar').catch((err: any) => {
|
||||
expect(shellChmodSpy).not.toHaveBeenCalled();
|
||||
expect(shellRmSpy).not.toHaveBeenCalled();
|
||||
expect(err).toBe('Test');
|
||||
done();
|
||||
});
|
||||
|
||||
cpExecCbs[0]('Test');
|
||||
});
|
||||
|
||||
|
||||
it('should abort and skip further operations if it fails to make non-writable', done => {
|
||||
(bc as any).extractArchive('foo', 'bar').catch((err: any) => {
|
||||
expect(shellChmodSpy).toHaveBeenCalled();
|
||||
expect(shellRmSpy).not.toHaveBeenCalled();
|
||||
expect(err).toBe('Test');
|
||||
done();
|
||||
});
|
||||
|
||||
shellChmodSpy.and.callFake(() => { throw 'Test'; });
|
||||
cpExecCbs[0]();
|
||||
});
|
||||
|
||||
|
||||
it('should abort and reject if it fails to remove the uploaded file', done => {
|
||||
(bc as any).extractArchive('foo', 'bar').catch((err: any) => {
|
||||
expect(shellChmodSpy).toHaveBeenCalled();
|
||||
expect(shellRmSpy).toHaveBeenCalled();
|
||||
expect(err).toBe('Test');
|
||||
done();
|
||||
});
|
||||
|
||||
shellRmSpy.and.callFake(() => { throw 'Test'; });
|
||||
cpExecCbs[0]();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
@ -1,61 +0,0 @@
|
||||
// Imports
|
||||
import {BuildEvent, CreatedBuildEvent} from '../../lib/upload-server/build-events';
|
||||
|
||||
// Tests
|
||||
describe('BuildEvent', () => {
|
||||
let evt: BuildEvent;
|
||||
|
||||
beforeEach(() => evt = new BuildEvent('foo', 42, 'bar'));
|
||||
|
||||
|
||||
it('should have a \'type\' property', () => {
|
||||
expect(evt.type).toBe('foo');
|
||||
});
|
||||
|
||||
|
||||
it('should have a \'pr\' property', () => {
|
||||
expect(evt.pr).toBe(42);
|
||||
});
|
||||
|
||||
|
||||
it('should have a \'sha\' property', () => {
|
||||
expect(evt.sha).toBe('bar');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
describe('CreatedBuildEvent', () => {
|
||||
let evt: CreatedBuildEvent;
|
||||
|
||||
beforeEach(() => evt = new CreatedBuildEvent(42, 'bar'));
|
||||
|
||||
|
||||
it('should have a static \'type\' property', () => {
|
||||
expect(CreatedBuildEvent.type).toBe('build.created');
|
||||
});
|
||||
|
||||
|
||||
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', () => {
|
||||
expect(evt.pr).toBe(42);
|
||||
});
|
||||
|
||||
|
||||
it('should have a \'sha\' property', () => {
|
||||
expect(evt.sha).toBe('bar');
|
||||
});
|
||||
|
||||
});
|
@ -1,261 +0,0 @@
|
||||
// Imports
|
||||
import * as jwt from 'jsonwebtoken';
|
||||
import {GithubPullRequests} from '../../lib/common/github-pull-requests';
|
||||
import {GithubTeams} from '../../lib/common/github-teams';
|
||||
import {BuildVerifier} from '../../lib/upload-server/build-verifier';
|
||||
import {expectToBeUploadError} from './helpers';
|
||||
|
||||
// Tests
|
||||
describe('BuildVerifier', () => {
|
||||
const defaultConfig = {
|
||||
allowedTeamSlugs: ['team1', 'team2'],
|
||||
githubToken: 'githubToken',
|
||||
organization: 'organization',
|
||||
repoSlug: 'repo/slug',
|
||||
secret: 'secret',
|
||||
};
|
||||
let bv: BuildVerifier;
|
||||
|
||||
// Helpers
|
||||
const createBuildVerifier = (partialConfig: Partial<typeof defaultConfig> = {}) => {
|
||||
const cfg = {...defaultConfig, ...partialConfig};
|
||||
return new BuildVerifier(cfg.secret, cfg.githubToken, cfg.repoSlug, cfg.organization,
|
||||
cfg.allowedTeamSlugs);
|
||||
};
|
||||
|
||||
beforeEach(() => bv = createBuildVerifier());
|
||||
|
||||
|
||||
describe('constructor()', () => {
|
||||
|
||||
['secret', 'githubToken', 'repoSlug', 'organization', 'allowedTeamSlugs'].forEach(param => {
|
||||
it(`should throw if '${param}' is missing or empty`, () => {
|
||||
expect(() => createBuildVerifier({[param]: ''})).
|
||||
toThrowError(`Missing or empty required parameter '${param}'!`);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should throw if \'allowedTeamSlugs\' is an empty array', () => {
|
||||
expect(() => createBuildVerifier({allowedTeamSlugs: []})).
|
||||
toThrowError('Missing or empty required parameter \'allowedTeamSlugs\'!');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
describe('verify()', () => {
|
||||
const pr = 9;
|
||||
const defaultJwt = {
|
||||
'exp': Math.floor(Date.now() / 1000) + 30,
|
||||
'iat': Math.floor(Date.now() / 1000) - 30,
|
||||
'iss': 'Travis CI, GmbH',
|
||||
'pull-request': pr,
|
||||
'slug': defaultConfig.repoSlug,
|
||||
};
|
||||
let bvGetPrAuthorTeamMembership: jasmine.Spy;
|
||||
|
||||
// Heleprs
|
||||
const createAuthHeader = (partialJwt: Partial<typeof defaultJwt> = {}, secret: string = defaultConfig.secret) =>
|
||||
`Token ${jwt.sign({...defaultJwt, ...partialJwt}, secret)}`;
|
||||
|
||||
beforeEach(() => {
|
||||
bvGetPrAuthorTeamMembership = spyOn(bv, 'getPrAuthorTeamMembership').
|
||||
and.returnValue(Promise.resolve({author: 'some-author', isMember: true}));
|
||||
});
|
||||
|
||||
|
||||
it('should return a promise', done => {
|
||||
const promise = bv.verify(pr, createAuthHeader());
|
||||
promise.then(done); // Do not complete the test (and release the spies) synchronously
|
||||
// to avoid running the actual `bvGetPrAuthorTeamMembership()`.
|
||||
|
||||
expect(promise).toEqual(jasmine.any(Promise));
|
||||
});
|
||||
|
||||
|
||||
it('should fail if the authorization header is invalid', done => {
|
||||
bv.verify(pr, 'foo').catch(err => {
|
||||
const errorMessage = 'Error while verifying upload for PR 9: jwt malformed';
|
||||
|
||||
expectToBeUploadError(err, 403, errorMessage);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should fail if the secret is invalid', done => {
|
||||
bv.verify(pr, createAuthHeader({}, 'foo')).catch(err => {
|
||||
const errorMessage = 'Error while verifying upload for PR 9: invalid signature';
|
||||
|
||||
expectToBeUploadError(err, 403, errorMessage);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should fail if the issuer is invalid', done => {
|
||||
bv.verify(pr, createAuthHeader({iss: 'not valid'})).catch(err => {
|
||||
const errorMessage = 'Error while verifying upload for PR 9: ' +
|
||||
`jwt issuer invalid. expected: ${defaultJwt.iss}`;
|
||||
|
||||
expectToBeUploadError(err, 403, errorMessage);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should fail if the token has expired', done => {
|
||||
bv.verify(pr, createAuthHeader({exp: 0})).catch(err => {
|
||||
const errorMessage = 'Error while verifying upload for PR 9: jwt expired';
|
||||
|
||||
expectToBeUploadError(err, 403, errorMessage);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should fail if the repo slug does not match', done => {
|
||||
bv.verify(pr, createAuthHeader({slug: 'foo/bar'})).catch(err => {
|
||||
const errorMessage = 'Error while verifying upload for PR 9: ' +
|
||||
`jwt slug invalid. expected: ${defaultConfig.repoSlug}`;
|
||||
|
||||
expectToBeUploadError(err, 403, errorMessage);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should fail if the PR does not match', done => {
|
||||
bv.verify(pr, createAuthHeader({'pull-request': 1337})).catch(err => {
|
||||
const errorMessage = 'Error while verifying upload for PR 9: ' +
|
||||
`jwt pull-request invalid. expected: ${pr}`;
|
||||
|
||||
expectToBeUploadError(err, 403, errorMessage);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should not fail if the token is valid', done => {
|
||||
bv.verify(pr, createAuthHeader()).then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should not fail even if the token has been issued in the future', done => {
|
||||
const in30s = Math.floor(Date.now() / 1000) + 30;
|
||||
bv.verify(pr, createAuthHeader({iat: in30s})).then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should call \'getPrAuthorTeamMembership()\' if the token is valid', done => {
|
||||
bv.verify(pr, createAuthHeader()).then(() => {
|
||||
expect(bvGetPrAuthorTeamMembership).toHaveBeenCalledWith(pr);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should fail if \'getPrAuthorTeamMembership()\' rejects', done => {
|
||||
bvGetPrAuthorTeamMembership.and.callFake(() => Promise.reject('Test'));
|
||||
bv.verify(pr, createAuthHeader()).catch(err => {
|
||||
expectToBeUploadError(err, 403, `Error while verifying upload for PR ${pr}: Test`);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should fail if \'getPrAuthorTeamMembership()\' reports no membership', done => {
|
||||
const errorMessage = `Error while verifying upload for PR ${pr}: User 'test' is not an active member of any of ` +
|
||||
'the following teams: team1, team2';
|
||||
|
||||
bvGetPrAuthorTeamMembership.and.returnValue(Promise.resolve({author: 'test', isMember: false}));
|
||||
bv.verify(pr, createAuthHeader()).catch(err => {
|
||||
expectToBeUploadError(err, 403, errorMessage);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should succeed if everything checks outs', done => {
|
||||
bv.verify(pr, createAuthHeader()).then(done);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
@ -1,11 +0,0 @@
|
||||
import {UploadError} from '../../lib/upload-server/upload-error';
|
||||
|
||||
export const expectToBeUploadError = (actual: UploadError, status?: number, message?: string) => {
|
||||
expect(actual).toEqual(jasmine.any(UploadError));
|
||||
if (status != null) {
|
||||
expect(actual.status).toBe(status);
|
||||
}
|
||||
if (message != null) {
|
||||
expect(actual.message).toBe(message);
|
||||
}
|
||||
};
|
@ -1,39 +0,0 @@
|
||||
// Imports
|
||||
import {UploadError} from '../../lib/upload-server/upload-error';
|
||||
|
||||
// Tests
|
||||
describe('UploadError', () => {
|
||||
let err: UploadError;
|
||||
|
||||
beforeEach(() => err = new UploadError(999, 'message'));
|
||||
|
||||
|
||||
it('should extend Error', () => {
|
||||
expect(err).toEqual(jasmine.any(UploadError));
|
||||
expect(err).toEqual(jasmine.any(Error));
|
||||
|
||||
expect(Object.getPrototypeOf(err)).toBe(UploadError.prototype);
|
||||
});
|
||||
|
||||
|
||||
it('should have a \'status\' property', () => {
|
||||
expect(err.status).toBe(999);
|
||||
});
|
||||
|
||||
|
||||
it('should have a \'message\' property', () => {
|
||||
expect(err.message).toBe('message');
|
||||
});
|
||||
|
||||
|
||||
it('should have a 500 \'status\' by default', () => {
|
||||
expect(new UploadError().status).toBe(500);
|
||||
});
|
||||
|
||||
|
||||
it('should have an empty \'message\' by default', () => {
|
||||
expect(new UploadError().message).toBe('');
|
||||
expect(new UploadError(999).message).toBe('');
|
||||
});
|
||||
|
||||
});
|
@ -1,403 +0,0 @@
|
||||
// Imports
|
||||
import * as express from 'express';
|
||||
import * as http from 'http';
|
||||
import * as supertest from 'supertest';
|
||||
import {GithubPullRequests} from '../../lib/common/github-pull-requests';
|
||||
import {BuildCreator} from '../../lib/upload-server/build-creator';
|
||||
import {CreatedBuildEvent} from '../../lib/upload-server/build-events';
|
||||
import {BuildVerifier} from '../../lib/upload-server/build-verifier';
|
||||
import {uploadServerFactory as usf} from '../../lib/upload-server/upload-server-factory';
|
||||
|
||||
// Tests
|
||||
describe('uploadServerFactory', () => {
|
||||
const defaultConfig = {
|
||||
buildsDir: 'builds/dir',
|
||||
domainName: 'domain.name',
|
||||
githubOrganization: 'organization',
|
||||
githubTeamSlugs: ['team1', 'team2'],
|
||||
githubToken: '12345',
|
||||
repoSlug: 'repo/slug',
|
||||
secret: 'secret',
|
||||
};
|
||||
|
||||
// Helpers
|
||||
const createUploadServer = (partialConfig: Partial<typeof defaultConfig> = {}) =>
|
||||
usf.create({...defaultConfig, ...partialConfig});
|
||||
|
||||
|
||||
describe('create()', () => {
|
||||
let usfCreateMiddlewareSpy: jasmine.Spy;
|
||||
|
||||
beforeEach(() => {
|
||||
usfCreateMiddlewareSpy = spyOn(usf as any, 'createMiddleware').and.callThrough();
|
||||
});
|
||||
|
||||
|
||||
it('should throw if \'buildsDir\' is missing or empty', () => {
|
||||
expect(() => createUploadServer({buildsDir: ''})).
|
||||
toThrowError('Missing or empty required parameter \'buildsDir\'!');
|
||||
});
|
||||
|
||||
|
||||
it('should throw if \'domainName\' is missing or empty', () => {
|
||||
expect(() => createUploadServer({domainName: ''})).
|
||||
toThrowError('Missing or empty required parameter \'domainName\'!');
|
||||
});
|
||||
|
||||
|
||||
it('should throw if \'githubToken\' is missing or empty', () => {
|
||||
expect(() => createUploadServer({githubToken: ''})).
|
||||
toThrowError('Missing or empty required parameter \'githubToken\'!');
|
||||
});
|
||||
|
||||
|
||||
it('should throw if \'githubOrganization\' is missing or empty', () => {
|
||||
expect(() => createUploadServer({githubOrganization: ''})).
|
||||
toThrowError('Missing or empty required parameter \'organization\'!');
|
||||
});
|
||||
|
||||
|
||||
it('should throw if \'githubTeamSlugs\' is missing or empty', () => {
|
||||
expect(() => createUploadServer({githubTeamSlugs: []})).
|
||||
toThrowError('Missing or empty required parameter \'allowedTeamSlugs\'!');
|
||||
});
|
||||
|
||||
|
||||
it('should throw if \'repoSlug\' is missing or empty', () => {
|
||||
expect(() => createUploadServer({repoSlug: ''})).
|
||||
toThrowError('Missing or empty required parameter \'repoSlug\'!');
|
||||
});
|
||||
|
||||
|
||||
it('should throw if \'secret\' is missing or empty', () => {
|
||||
expect(() => createUploadServer({secret: ''})).
|
||||
toThrowError('Missing or empty required parameter \'secret\'!');
|
||||
});
|
||||
|
||||
|
||||
it('should return an http.Server', () => {
|
||||
const httpCreateServerSpy = spyOn(http, 'createServer').and.callThrough();
|
||||
const server = createUploadServer();
|
||||
|
||||
expect(server).toBe(httpCreateServerSpy.calls.mostRecent().returnValue);
|
||||
});
|
||||
|
||||
|
||||
it('should create and use an appropriate BuildCreator', () => {
|
||||
const usfCreateBuildCreatorSpy = spyOn(usf as any, 'createBuildCreator').and.callThrough();
|
||||
|
||||
createUploadServer();
|
||||
const buildCreator: BuildCreator = usfCreateBuildCreatorSpy.calls.mostRecent().returnValue;
|
||||
|
||||
expect(usfCreateMiddlewareSpy).toHaveBeenCalledWith(jasmine.any(BuildVerifier), buildCreator);
|
||||
expect(usfCreateBuildCreatorSpy).toHaveBeenCalledWith('builds/dir', '12345', 'repo/slug', 'domain.name');
|
||||
});
|
||||
|
||||
|
||||
it('should create and use an appropriate middleware', () => {
|
||||
const httpCreateServerSpy = spyOn(http, 'createServer').and.callThrough();
|
||||
|
||||
createUploadServer();
|
||||
const middleware: express.Express = usfCreateMiddlewareSpy.calls.mostRecent().returnValue;
|
||||
const buildVerifier = jasmine.any(BuildVerifier);
|
||||
const buildCreator = jasmine.any(BuildCreator);
|
||||
|
||||
expect(httpCreateServerSpy).toHaveBeenCalledWith(middleware);
|
||||
expect(usfCreateMiddlewareSpy).toHaveBeenCalledWith(buildVerifier, buildCreator);
|
||||
});
|
||||
|
||||
|
||||
it('should log the server address info on \'listening\'', () => {
|
||||
const consoleInfoSpy = spyOn(console, 'info');
|
||||
const server = createUploadServer('builds/dir');
|
||||
server.address = () => ({address: 'foo', family: '', port: 1337});
|
||||
|
||||
expect(consoleInfoSpy).not.toHaveBeenCalled();
|
||||
|
||||
server.emit('listening');
|
||||
expect(consoleInfoSpy).toHaveBeenCalledWith('Up and running (and listening on foo:1337)...');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
// Protected methods
|
||||
|
||||
describe('createBuildCreator()', () => {
|
||||
let buildCreator: BuildCreator;
|
||||
|
||||
beforeEach(() => {
|
||||
buildCreator = (usf as any).createBuildCreator(
|
||||
defaultConfig.buildsDir,
|
||||
defaultConfig.githubToken,
|
||||
defaultConfig.repoSlug,
|
||||
defaultConfig.domainName,
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
it('should pass the \'buildsDir\' to the BuildCreator', () => {
|
||||
expect((buildCreator as any).buildsDir).toBe('builds/dir');
|
||||
});
|
||||
|
||||
|
||||
it('should post a comment on GitHub on \'build.created\'', () => {
|
||||
const prsAddCommentSpy = spyOn(GithubPullRequests.prototype, 'addComment');
|
||||
const commentBody = 'The angular.io preview for 1234567 is available [here][1].\n\n' +
|
||||
'[1]: https://pr42-1234567890.domain.name/';
|
||||
|
||||
buildCreator.emit(CreatedBuildEvent.type, {pr: 42, sha: '1234567890'});
|
||||
|
||||
expect(prsAddCommentSpy).toHaveBeenCalledWith(42, commentBody);
|
||||
});
|
||||
|
||||
|
||||
it('should pass the correct \'githubToken\' and \'repoSlug\' to GithubPullRequests', () => {
|
||||
const prsAddCommentSpy = spyOn(GithubPullRequests.prototype, 'addComment');
|
||||
|
||||
buildCreator.emit(CreatedBuildEvent.type, {pr: 42, sha: '1234567890'});
|
||||
const prs = prsAddCommentSpy.calls.mostRecent().object;
|
||||
|
||||
expect(prs).toEqual(jasmine.any(GithubPullRequests));
|
||||
expect((prs as any).repoSlug).toBe('repo/slug');
|
||||
expect((prs as any).requestHeaders.Authorization).toContain('12345');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
describe('createMiddleware()', () => {
|
||||
let buildVerifier: BuildVerifier;
|
||||
let buildCreator: BuildCreator;
|
||||
let agent: supertest.SuperTest<supertest.Test>;
|
||||
|
||||
// Helpers
|
||||
const promisifyRequest = (req: supertest.Request) =>
|
||||
new Promise((resolve, reject) => req.end(err => err ? reject(err) : resolve()));
|
||||
const verifyRequests = (reqs: supertest.Request[], done: jasmine.DoneFn) =>
|
||||
Promise.all(reqs.map(promisifyRequest)).then(done, done.fail);
|
||||
|
||||
beforeEach(() => {
|
||||
buildVerifier = new BuildVerifier(
|
||||
defaultConfig.secret,
|
||||
defaultConfig.githubToken,
|
||||
defaultConfig.repoSlug,
|
||||
defaultConfig.githubOrganization,
|
||||
defaultConfig.githubTeamSlugs,
|
||||
);
|
||||
buildCreator = new BuildCreator(defaultConfig.buildsDir);
|
||||
agent = supertest.agent((usf as any).createMiddleware(buildVerifier, buildCreator));
|
||||
|
||||
spyOn(console, 'error');
|
||||
});
|
||||
|
||||
|
||||
describe('GET /create-build/<pr>/<sha>', () => {
|
||||
const pr = '9';
|
||||
const sha = '9'.repeat(40);
|
||||
let buildVerifierVerifySpy: jasmine.Spy;
|
||||
let buildCreatorCreateSpy: jasmine.Spy;
|
||||
|
||||
beforeEach(() => {
|
||||
buildVerifierVerifySpy = spyOn(buildVerifier, 'verify').and.returnValue(Promise.resolve());
|
||||
buildCreatorCreateSpy = spyOn(buildCreator, 'create').and.returnValue(Promise.resolve());
|
||||
});
|
||||
|
||||
|
||||
it('should respond with 405 for non-GET requests', done => {
|
||||
verifyRequests([
|
||||
agent.put(`/create-build/${pr}/${sha}`).expect(405),
|
||||
agent.post(`/create-build/${pr}/${sha}`).expect(405),
|
||||
agent.patch(`/create-build/${pr}/${sha}`).expect(405),
|
||||
agent.delete(`/create-build/${pr}/${sha}`).expect(405),
|
||||
], done);
|
||||
});
|
||||
|
||||
|
||||
it('should respond with 401 for requests without an \'AUTHORIZATION\' header', done => {
|
||||
const url = `/create-build/${pr}/${sha}`;
|
||||
const responseBody = `Missing or empty 'AUTHORIZATION' header in request: GET ${url}`;
|
||||
|
||||
verifyRequests([
|
||||
agent.get(url).expect(401, responseBody),
|
||||
agent.get(url).set('AUTHORIZATION', '').expect(401, responseBody),
|
||||
], done);
|
||||
});
|
||||
|
||||
|
||||
it('should respond with 400 for requests without an \'X-FILE\' header', done => {
|
||||
const url = `/create-build/${pr}/${sha}`;
|
||||
const responseBody = `Missing or empty 'X-FILE' header in request: GET ${url}`;
|
||||
|
||||
const request1 = agent.get(url).set('AUTHORIZATION', 'foo');
|
||||
const request2 = agent.get(url).set('AUTHORIZATION', 'foo').set('X-FILE', '');
|
||||
|
||||
verifyRequests([
|
||||
request1.expect(400, responseBody),
|
||||
request2.expect(400, responseBody),
|
||||
], done);
|
||||
});
|
||||
|
||||
|
||||
it('should respond with 404 for unknown paths', done => {
|
||||
verifyRequests([
|
||||
agent.get(`/foo/create-build/${pr}/${sha}`).expect(404),
|
||||
agent.get(`/foo-create-build/${pr}/${sha}`).expect(404),
|
||||
agent.get(`/fooncreate-build/${pr}/${sha}`).expect(404),
|
||||
agent.get(`/create-build/foo/${pr}/${sha}`).expect(404),
|
||||
agent.get(`/create-build-foo/${pr}/${sha}`).expect(404),
|
||||
agent.get(`/create-buildnfoo/${pr}/${sha}`).expect(404),
|
||||
agent.get(`/create-build/pr${pr}/${sha}`).expect(404),
|
||||
agent.get(`/create-build/${pr}/${sha}42`).expect(404),
|
||||
], done);
|
||||
});
|
||||
|
||||
|
||||
it('should call \'BuildVerifier#verify()\' with the correct arguments', done => {
|
||||
const req = agent.
|
||||
get(`/create-build/${pr}/${sha}`).
|
||||
set('AUTHORIZATION', 'foo').
|
||||
set('X-FILE', 'bar');
|
||||
|
||||
promisifyRequest(req).
|
||||
then(() => expect(buildVerifierVerifySpy).toHaveBeenCalledWith(9, 'foo')).
|
||||
then(done, done.fail);
|
||||
});
|
||||
|
||||
|
||||
it('should propagate errors from BuildVerifier', done => {
|
||||
buildVerifierVerifySpy.and.callFake(() => Promise.reject('Test'));
|
||||
|
||||
const req = agent.
|
||||
get(`/create-build/${pr}/${sha}`).
|
||||
set('AUTHORIZATION', 'foo').
|
||||
set('X-FILE', 'bar').
|
||||
expect(500, 'Test');
|
||||
|
||||
promisifyRequest(req).
|
||||
then(() => {
|
||||
expect(buildVerifierVerifySpy).toHaveBeenCalledWith(9, 'foo');
|
||||
expect(buildCreatorCreateSpy).not.toHaveBeenCalled();
|
||||
}).
|
||||
then(done, done.fail);
|
||||
});
|
||||
|
||||
|
||||
it('should call \'BuildCreator#create()\' with the correct arguments', done => {
|
||||
const req = agent.
|
||||
get(`/create-build/${pr}/${sha}`).
|
||||
set('AUTHORIZATION', 'foo').
|
||||
set('X-FILE', 'bar');
|
||||
|
||||
promisifyRequest(req).
|
||||
then(() => expect(buildCreatorCreateSpy).toHaveBeenCalledWith(pr, sha, 'bar')).
|
||||
then(done, done.fail);
|
||||
});
|
||||
|
||||
|
||||
it('should propagate errors from BuildCreator', done => {
|
||||
buildCreatorCreateSpy.and.callFake(() => Promise.reject('Test'));
|
||||
const req = agent.
|
||||
get(`/create-build/${pr}/${sha}`).
|
||||
set('AUTHORIZATION', 'foo').
|
||||
set('X-FILE', 'bar').
|
||||
expect(500, 'Test');
|
||||
|
||||
verifyRequests([req], done);
|
||||
});
|
||||
|
||||
|
||||
it('should respond with 201 on successful upload', done => {
|
||||
const req = agent.
|
||||
get(`/create-build/${pr}/${sha}`).
|
||||
set('AUTHORIZATION', 'foo').
|
||||
set('X-FILE', 'bar').
|
||||
expect(201, http.STATUS_CODES[201]);
|
||||
|
||||
verifyRequests([req], done);
|
||||
});
|
||||
|
||||
|
||||
it('should reject PRs with leading zeros', done => {
|
||||
verifyRequests([agent.get(`/create-build/0${pr}/${sha}`).expect(404)], done);
|
||||
});
|
||||
|
||||
|
||||
it('should accept SHAs with leading zeros (but not trim the zeros)', done => {
|
||||
const sha40 = '0'.repeat(40);
|
||||
const sha41 = `0${sha40}`;
|
||||
|
||||
const request40 = agent.get(`/create-build/${pr}/${sha40}`).set('AUTHORIZATION', 'foo').set('X-FILE', 'bar');
|
||||
const request41 = agent.get(`/create-build/${pr}/${sha41}`).set('AUTHORIZATION', 'baz').set('X-FILE', 'qux');
|
||||
|
||||
Promise.all([
|
||||
promisifyRequest(request40.expect(201)),
|
||||
promisifyRequest(request41.expect(404)),
|
||||
]).then(done, done.fail);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
describe('GET /health-check', () => {
|
||||
|
||||
it('should respond with 200', done => {
|
||||
verifyRequests([
|
||||
agent.get('/health-check').expect(200),
|
||||
agent.get('/health-check/').expect(200),
|
||||
], done);
|
||||
});
|
||||
|
||||
|
||||
it('should respond with 405 for non-GET requests', done => {
|
||||
verifyRequests([
|
||||
agent.put('/health-check').expect(405),
|
||||
agent.post('/health-check').expect(405),
|
||||
agent.patch('/health-check').expect(405),
|
||||
agent.delete('/health-check').expect(405),
|
||||
], done);
|
||||
});
|
||||
|
||||
|
||||
it('should respond with 404 if the path does not match exactly', done => {
|
||||
verifyRequests([
|
||||
agent.get('/health-check/foo').expect(404),
|
||||
agent.get('/health-check-foo').expect(404),
|
||||
agent.get('/health-checknfoo').expect(404),
|
||||
agent.get('/foo/health-check').expect(404),
|
||||
agent.get('/foo-health-check').expect(404),
|
||||
agent.get('/foonhealth-check').expect(404),
|
||||
], done);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
describe('GET *', () => {
|
||||
|
||||
it('should respond with 404', done => {
|
||||
const responseBody = 'Unknown resource in request: GET /some/url';
|
||||
verifyRequests([agent.get('/some/url').expect(404, responseBody)], done);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
describe('ALL *', () => {
|
||||
|
||||
it('should respond with 405', done => {
|
||||
const responseFor = (method: string) => `Unsupported method in request: ${method.toUpperCase()} /some/url`;
|
||||
|
||||
verifyRequests([
|
||||
agent.put('/some/url').expect(405, responseFor('put')),
|
||||
agent.post('/some/url').expect(405, responseFor('post')),
|
||||
agent.patch('/some/url').expect(405, responseFor('patch')),
|
||||
agent.delete('/some/url').expect(405, responseFor('delete')),
|
||||
], done);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
@ -1,28 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"alwaysStrict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"inlineSourceMap": true,
|
||||
"lib": [
|
||||
"es2016"
|
||||
],
|
||||
"noImplicitAny": true,
|
||||
"noImplicitReturns": true,
|
||||
"noImplicitThis": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"outDir": "dist",
|
||||
"pretty": true,
|
||||
"rootDir": ".",
|
||||
"skipLibCheck": true,
|
||||
"strictNullChecks": true,
|
||||
"target": "es5",
|
||||
"typeRoots": [
|
||||
"node_modules/@types"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"lib/**/*",
|
||||
"test/**/*"
|
||||
]
|
||||
}
|
@ -1,15 +0,0 @@
|
||||
{
|
||||
"extends": "tslint:recommended",
|
||||
"rules": {
|
||||
"array-type": [true, "array"],
|
||||
"arrow-parens": [true, "ban-single-arg-parens"],
|
||||
"interface-name": [true, "never-prefix"],
|
||||
"max-classes-per-file": [true, 4],
|
||||
"no-consecutive-blank-lines": [true, 2],
|
||||
"no-console": false,
|
||||
"no-namespace": [true, "allow-declarations"],
|
||||
"no-string-literal": false,
|
||||
"quotemark": [true, "single"],
|
||||
"variable-name": [true, "ban-keywords", "check-format", "allow-leading-underscore"]
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -1,8 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -e -o pipefail
|
||||
|
||||
# Set up env variables
|
||||
export AIO_GITHUB_TOKEN=$(head -c -1 /aio-secrets/GITHUB_TOKEN 2>/dev/null)
|
||||
|
||||
# Run the clean-up
|
||||
node $AIO_SCRIPTS_JS_DIR/dist/lib/clean-up >> /var/log/aio/clean-up.log 2>&1
|
@ -1,53 +0,0 @@
|
||||
#!/bin/bash
|
||||
set +e -o pipefail
|
||||
|
||||
|
||||
# Variables
|
||||
exitCode=0
|
||||
|
||||
|
||||
# Helpers
|
||||
function reportStatus {
|
||||
local lastExitCode=$?
|
||||
echo "$1: $([[ $lastExitCode -eq 0 ]] && echo OK || echo NOT OK)"
|
||||
[[ $lastExitCode -eq 0 ]] || exitCode=1
|
||||
}
|
||||
|
||||
|
||||
# Check services
|
||||
services=(
|
||||
rsyslog
|
||||
cron
|
||||
nginx
|
||||
pm2-root
|
||||
)
|
||||
for s in ${services[@]}; do
|
||||
service $s status > /dev/null
|
||||
reportStatus "Service '$s'"
|
||||
done
|
||||
|
||||
|
||||
# Check servers
|
||||
origins=(
|
||||
http://$AIO_UPLOAD_HOSTNAME:$AIO_UPLOAD_PORT
|
||||
http://$AIO_NGINX_HOSTNAME:$AIO_NGINX_PORT_HTTP
|
||||
https://$AIO_NGINX_HOSTNAME:$AIO_NGINX_PORT_HTTPS
|
||||
)
|
||||
for o in ${origins[@]}; do
|
||||
curl --fail --silent $o/health-check > /dev/null
|
||||
reportStatus "Server '$o'"
|
||||
done
|
||||
|
||||
|
||||
# Check resolution of external URLs
|
||||
origins=(
|
||||
https://google.com
|
||||
)
|
||||
for o in ${origins[@]}; do
|
||||
curl --fail --silent $o > /dev/null
|
||||
reportStatus "External URL '$o'"
|
||||
done
|
||||
|
||||
|
||||
# Exit
|
||||
exit $exitCode
|
@ -1,18 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -e -o pipefail
|
||||
|
||||
exec >> /var/log/aio/init.log
|
||||
exec 2>&1
|
||||
|
||||
# Start the services
|
||||
echo [`date`] - Starting services...
|
||||
mkdir -p $AIO_NGINX_LOGS_DIR
|
||||
mkdir -p $TEST_AIO_NGINX_LOGS_DIR
|
||||
|
||||
service rsyslog start
|
||||
service cron start
|
||||
service dnsmasq start
|
||||
service nginx start
|
||||
service pm2-root start
|
||||
aio-upload-server-prod start
|
||||
echo [`date`] - Services started successfully.
|
@ -1,15 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -e -o pipefail
|
||||
|
||||
# Set up env variables for production
|
||||
export AIO_GITHUB_TOKEN=$(head -c -1 /aio-secrets/GITHUB_TOKEN 2>/dev/null)
|
||||
export AIO_PREVIEW_DEPLOYMENT_TOKEN=$(head -c -1 /aio-secrets/PREVIEW_DEPLOYMENT_TOKEN 2>/dev/null)
|
||||
|
||||
# Start the upload-server instance
|
||||
# TODO(gkalpak): Ideally, the upload server should be run as a non-privileged user.
|
||||
# (Currently, there doesn't seem to be a straight forward way.)
|
||||
action=$([ "$1" == "stop" ] && echo "stop" || echo "start")
|
||||
pm2 $action $AIO_SCRIPTS_JS_DIR/dist/lib/upload-server \
|
||||
--log /var/log/aio/upload-server-prod.log \
|
||||
--name aio-upload-server-prod \
|
||||
${@:2}
|
@ -1,29 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -e -o pipefail
|
||||
|
||||
# Set up env variables for testing
|
||||
export AIO_BUILDS_DIR=$TEST_AIO_BUILDS_DIR
|
||||
export AIO_DOMAIN_NAME=$TEST_AIO_DOMAIN_NAME
|
||||
export AIO_GITHUB_ORGANIZATION=$TEST_AIO_GITHUB_ORGANIZATION
|
||||
export AIO_GITHUB_TEAM_SLUGS=$TEST_AIO_GITHUB_TEAM_SLUGS
|
||||
export AIO_PREVIEW_DEPLOYMENT_TOKEN=$TEST_AIO_PREVIEW_DEPLOYMENT_TOKEN
|
||||
export AIO_REPO_SLUG=$TEST_AIO_REPO_SLUG
|
||||
export AIO_UPLOAD_HOSTNAME=$TEST_AIO_UPLOAD_HOSTNAME
|
||||
export AIO_UPLOAD_PORT=$TEST_AIO_UPLOAD_PORT
|
||||
|
||||
export AIO_GITHUB_TOKEN=$(head -c -1 /aio-secrets/TEST_GITHUB_TOKEN 2>/dev/null || echo "TEST_GITHUB_TOKEN")
|
||||
export AIO_PREVIEW_DEPLOYMENT_TOKEN=$(head -c -1 /aio-secrets/TEST_PREVIEW_DEPLOYMENT_TOKEN 2>/dev/null || echo "TEST_PREVIEW_DEPLOYMENT_TOKEN")
|
||||
|
||||
# Start the upload-server instance
|
||||
# TODO(gkalpak): Ideally, the upload server should be run as a non-privileged user.
|
||||
# (Currently, there doesn't seem to be a straight forward way.)
|
||||
appName=aio-upload-server-test
|
||||
if [[ "$1" == "stop" ]]; then
|
||||
pm2 delete $appName
|
||||
else
|
||||
pm2 start $AIO_SCRIPTS_JS_DIR/dist/lib/upload-server/index-test.js \
|
||||
--log /var/log/aio/upload-server-test.log \
|
||||
--name $appName \
|
||||
--no-autorestart \
|
||||
${@:2}
|
||||
fi
|
@ -1,40 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -e -o pipefail
|
||||
|
||||
logFile=/var/log/aio/verify-setup.log
|
||||
uploadServerLogFile=/var/log/aio/upload-server-verify-setup.log
|
||||
|
||||
exec 3>&1
|
||||
exec >> $logFile
|
||||
exec 2>&1
|
||||
|
||||
echo "[`date`] - Starting verification..."
|
||||
|
||||
# Helpers
|
||||
function countdown {
|
||||
message=$1
|
||||
secs=$2
|
||||
while [ $secs -gt 0 ]; do
|
||||
echo -ne "$message in $secs...\033[0K\r"
|
||||
sleep 1
|
||||
: $((secs--))
|
||||
done
|
||||
echo -ne "\033[0K\r"
|
||||
}
|
||||
|
||||
function onExit {
|
||||
aio-upload-server-test stop
|
||||
echo -e "Full logs in '$logFile'.\n" > /dev/fd/3
|
||||
}
|
||||
|
||||
# Setup EXIT trap
|
||||
trap 'onExit' EXIT
|
||||
|
||||
# Start an upload-server instance for testing
|
||||
aio-upload-server-test start --log $uploadServerLogFile
|
||||
|
||||
# Give the upload-server some time to start :(
|
||||
countdown "Starting" 5 > /dev/fd/3
|
||||
|
||||
# Run the tests
|
||||
node $AIO_SCRIPTS_JS_DIR/dist/lib/verify-setup | tee /dev/fd/3
|
@ -1,28 +0,0 @@
|
||||
# VM Setup Instructions
|
||||
|
||||
|
||||
## Overview
|
||||
- [General overview](overview--general.md)
|
||||
- [Security model](overview--security-model.md)
|
||||
- [Available Commands](overview--scripts-and-commands.md)
|
||||
|
||||
|
||||
## Setting up the VM
|
||||
- [Set up secrets](vm-setup--set-up-secrets.md)
|
||||
- [Set up docker](vm-setup--set-up-docker.md)
|
||||
- [Attach persistent disk](vm-setup--attach-persistent-disk.md)
|
||||
- [Create host directories and files](vm-setup--create-host-dirs-and-files.md)
|
||||
- [Create docker image](vm-setup--create-docker-image.md)
|
||||
|
||||
|
||||
## Configuring the docker image
|
||||
- [Available environment variables](image-config--environment-variables.md)
|
||||
|
||||
|
||||
## Starting the docker container
|
||||
- [Create docker image](vm-setup--start-docker-container.md)
|
||||
|
||||
|
||||
## Miscellaneous
|
||||
- [Debug docker container](misc--debug-docker-container.md)
|
||||
- [Integrate with CI](misc--integrate-with-ci.md)
|
@ -1,52 +0,0 @@
|
||||
# Image config - Environment variables
|
||||
|
||||
|
||||
Below is a list of environment variables that can be configured when creating the docker image (as
|
||||
described [here](vm-setup--create-docker-image.md)). An up-to-date list of the configurable
|
||||
environment variables and their default values can be found in the
|
||||
[Dockerfile](../dockerbuild/Dockerfile).
|
||||
|
||||
**Note:**
|
||||
Each variable has a `TEST_` prefixed counterpart, which is used for testing purposes. In most cases
|
||||
you don't need to specify values for those.
|
||||
|
||||
- `AIO_BUILDS_DIR`:
|
||||
The directory (inside the container) where the uploaded build artifacts are kept.
|
||||
|
||||
- `AIO_DOMAIN_NAME`:
|
||||
The domain name of the server.
|
||||
|
||||
- `AIO_GITHUB_ORGANIZATION`:
|
||||
The GitHub organization whose teams arew whitelisted for accepting uploads.
|
||||
See also `AIO_GITHUB_TEAM_SLUGS`.
|
||||
|
||||
- `AIO_GITHUB_TEAM_SLUGS`:
|
||||
A comma-separated list of teams, whose authors are allowed to upload PRs.
|
||||
See also `AIO_GITHUB_ORGANIZATION`.
|
||||
|
||||
- `AIO_NGINX_HOSTNAME`:
|
||||
The internal hostname for accessing the nginx server. This is mostly used for performing a
|
||||
periodic health-check.
|
||||
|
||||
- `AIO_NGINX_PORT_HTTP`:
|
||||
The port number on which nginx listens for HTTP connections. This should be mapped to the
|
||||
corresponding port on the host VM (as described [here](vm-setup--start-docker-container.md)).
|
||||
|
||||
- `AIO_NGINX_PORT_HTTPS`:
|
||||
The port number on which nginx listens for HTTPS connections. This should be mapped to the
|
||||
corresponding port on the host VM (as described [here](vm-setup--start-docker-container.md)).
|
||||
|
||||
- `AIO_REPO_SLUG`:
|
||||
The repository slug (in the form `<user>/<repo>`) for which PRs will be uploaded.
|
||||
|
||||
- `AIO_UPLOAD_HOSTNAME`:
|
||||
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.
|
||||
|
||||
- `AIO_UPLOAD_MAX_SIZE`:
|
||||
The maximum allowed size for the uploaded gzip archive containing the build artifacts. Files
|
||||
larger than this will be rejected.
|
||||
|
||||
- `AIO_UPLOAD_PORT`:
|
||||
The port number on which the Node.js upload-server listens for HTTP connections. This is used by
|
||||
nginx for delegating upload requests and also for performing a periodic health-check.
|
@ -1,12 +0,0 @@
|
||||
# Miscellaneous - Debug docker container
|
||||
|
||||
|
||||
TODO (gkalpak): Add docs. Mention:
|
||||
- `aio-health-check`
|
||||
- `aio-verify-setup`
|
||||
- Test nginx accessible at:
|
||||
- `http://$TEST_AIO_NGINX_HOTNAME:$TEST_AIO_NGINX_PORT_HTTP`
|
||||
- `https://$TEST_AIO_NGINX_HOTNAME:$TEST_AIO_NGINX_PORT_HTTPS`
|
||||
- Test upload-server accessible at:
|
||||
- `http://$TEST_AIO_UPLOAD_HOTNAME:$TEST_AIO_UPLOAD_PORT`
|
||||
- Local DNS (via dnsmasq) maps the above hostnames to 127.0.0.1
|
@ -1,12 +0,0 @@
|
||||
# Miscellaneous - Integrate with CI
|
||||
|
||||
|
||||
TODO (gkalpak): Add docs. Mention:
|
||||
- Travis' JWT addon (+ limitations).
|
||||
Relevant files: `.travis.yml`
|
||||
- Testing on CI.
|
||||
Relevant files: `ci/test-aio.sh`, `aio/aio-builds-setup/scripts/test.sh`
|
||||
- Preverifying on CI.
|
||||
Relevant files: `ci/deploy.sh`, `aio/aio-builds-setup/scripts/travis-preverify-pr.sh`
|
||||
- Deploying from CI.
|
||||
Relevant files: `ci/deploy.sh`, `aio/scripts/deploy-preview.sh`
|
@ -1,84 +0,0 @@
|
||||
# Overview - General
|
||||
|
||||
|
||||
## Objective
|
||||
Whenever a PR job is run on Travis, we want to build `angular.io` and upload the build artifacts to
|
||||
a publicly accessible server so that collaborators (developers, designers, authors, etc) can preview
|
||||
the changes without having to checkout and build the app locally.
|
||||
|
||||
|
||||
## Source code
|
||||
In order to make it easier to administer the server and version-control the setup, we are using
|
||||
[docker](https://www.docker.com) to run a container on a VM. The Dockerfile and all other files
|
||||
necessary for creating the docker container are stored (and versioned) along with the angular.io
|
||||
project's source code (currently part of the angular/angular repo) in the `aio-builds-setup/`
|
||||
directory.
|
||||
|
||||
|
||||
## Setup
|
||||
The VM is hosted on [Google Compute Engine](https://cloud.google.com/compute/). The host OS is
|
||||
debian:jessie. For more info how to set up the host VM take a look at the "Setting up the VM"
|
||||
section in [TOC](_TOC.md).
|
||||
|
||||
|
||||
## Security model
|
||||
Since we are managing a public server, it is important to take appropriate measures in order to
|
||||
prevent abuse. For more details on the challenges and the chosen approach take a look at the
|
||||
[security model](overview--security-model.md).
|
||||
|
||||
|
||||
## The 10000 feet view
|
||||
This section gives a brief summary of the several operations performed on CI and by the docker
|
||||
container:
|
||||
|
||||
|
||||
### On CI (Travis)
|
||||
- Build job completes successfully (i.e. build succeeds and tests pass).
|
||||
- The CI script checks whether the build job was initiated by a PR against the angular/angular
|
||||
master branch.
|
||||
- The CI script checks whether the PR has touched any files inside the angular.io project directory
|
||||
(currently `aio/`).
|
||||
- The CI script checks whether the author of the PR is a member of one of the whitelisted GitHub
|
||||
teams (and therefore allowed to upload).
|
||||
**Note:**
|
||||
For security reasons, the same checks will be performed on the server as well. This is an optional
|
||||
step with the purpose of:
|
||||
1. Avoiding the wasted overhead associated with uploads that are going to be rejected (e.g.
|
||||
building the artifacts, sending them to the server, running checks on the server, etc).
|
||||
2. Avoiding failing the build (due to an error response from the server) or requiring additional
|
||||
logic for detecting the reasons of the failure.
|
||||
- The CI script gzip and upload 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).
|
||||
|
||||
|
||||
### Uploading build artifacts
|
||||
- nginx receives upload request.
|
||||
- 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.
|
||||
- The upload-server verifies that the uploaded file is not trying to overwrite an existing build,
|
||||
and runs several checks to determine whether the request should be accepted (more details can be
|
||||
found [here](overview--security-model.md)).
|
||||
- The upload-server deploys the artifacts to a sub-directory named after the PR number and SHA:
|
||||
`<PR>/<SHA>/`
|
||||
- The upload-server posts a comment on the corresponding PR on GitHub mentioning the SHA and the
|
||||
the link where the preview can be found.
|
||||
|
||||
|
||||
### Serving build artifacts
|
||||
- nginx receives a request for an uploaded resource on a subdomain corresponding to the PR and SHA.
|
||||
E.g.: `pr<PR>-<SHA>.ngbuilds.io/path/to/resurce`
|
||||
- nginx maps the subdomain to the correct sub-direcory and serves the resource.
|
||||
E.g.: `/<PR>/<SHA>/path/to/resource`
|
||||
|
||||
|
||||
### Removing obsolete artifacts
|
||||
In order to avoid flooding the disk with unnecessary build artifacts, there is a cronjob that runs a
|
||||
clean-up tasks once a day. The task retrieves all open PRs from GitHub and removes all directories
|
||||
that do not correspond with an open PR.
|
||||
|
||||
|
||||
### Health-check
|
||||
The docker service runs a periodic health-check that verifies the running conditions of the
|
||||
container. This includes verifying the status of specific system services, the responsiveness of
|
||||
nginx and the upload-server and internet connectivity.
|
@ -1,55 +0,0 @@
|
||||
# Overview - Scripts and Commands
|
||||
|
||||
|
||||
This is an overview of the available scripts and commands.
|
||||
|
||||
|
||||
## Scripts
|
||||
The scripts are located inside `<aio-builds-setup-dir>/scripts/`. The following scripts are
|
||||
available:
|
||||
|
||||
- `build.sh`:
|
||||
Can be used for creating a preconfigured docker image.
|
||||
See [here](vm-setup--create-docker-image.md) for more info.
|
||||
|
||||
- `test.sh`
|
||||
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.
|
||||
|
||||
- `travis-preverify-pr.sh`
|
||||
Can be used for "preverifying" a PR before uploading the artifacts to the server. It checks that
|
||||
the author of the PR a member of one of the specified GitHub teams and therefore allowed to upload
|
||||
build artifacts. This is useful for CI integration. See [here](misc--integrate-with-ci.md) for
|
||||
more info.
|
||||
|
||||
|
||||
## Commands
|
||||
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
|
||||
purposes. Each command is backed by a corresponding script inside
|
||||
`<aio-builds-setup-dir>/dockerbuild/scripts-sh/`.
|
||||
|
||||
- `aio-clean-up`:
|
||||
Cleans up the builds directory by removing the artifacts that do not correspond to an open PR.
|
||||
_It is run as a daily cronjob._
|
||||
|
||||
- `aio-health-check`:
|
||||
Runs a basic health-check, verifying that the necessary services are running, the servers are
|
||||
responding and there is a working internet connection.
|
||||
_It is used periodically by docker for determining the container's health status._
|
||||
|
||||
- `aio-init`:
|
||||
Initializes the container (mainly by starting the necessary services).
|
||||
_It is run (by default) when starting the container._
|
||||
|
||||
- `aio-upload-server-prod`:
|
||||
Spins up a Node.js upload-server instance.
|
||||
_It is used in `aio-init` (see above) during initialization._
|
||||
|
||||
- `aio-upload-server-test`:
|
||||
Spins up a Node.js upload-server instance for tests.
|
||||
_It is used in `aio-verify-setup` (see below) for running tests._
|
||||
|
||||
- `aio-verify-setup`:
|
||||
Runs a suite of e2e-like tests, mainly verifying the correct (inter)operation of nginx and the
|
||||
Node.js upload-server.
|
@ -1,116 +0,0 @@
|
||||
# Overview - Security model
|
||||
|
||||
|
||||
Whenever a PR job is run on Travis, we want to build `angular.io` and upload the build artifacts to
|
||||
a publicly accessible server so that collaborators (developers, designers, authors, etc) can preview
|
||||
the changes without having to checkout and build the app locally.
|
||||
|
||||
This document discusses the security considerations associated with uploading build artifacts as
|
||||
part of the CI setup and serving them publicly.
|
||||
|
||||
|
||||
## Security objectives
|
||||
|
||||
- **Prevent uploading arbitrary content to our servers.**
|
||||
Since there is no restriction on who can submit a PR, we cannot allow any PR's build artifacts to
|
||||
be uploaded.
|
||||
|
||||
- **Prevent overwriting other peoples uploaded content.**
|
||||
There needs to be a mechanism in place to ensure that the uploaded content does indeed correspond
|
||||
to the PR indicated by its URL.
|
||||
|
||||
- **Prevent arbitrary access on the server.**
|
||||
Since the PR author has full access over the build artifacts that would be uploaded, we must
|
||||
ensure that the uploaded files will not enable arbitrary access to the server or expose sensitive
|
||||
info.
|
||||
|
||||
|
||||
## Issues / Caveats
|
||||
|
||||
- Because the PR author can change the scripts run on CI, any security mechanisms must be immune to
|
||||
such changes.
|
||||
|
||||
- For security reasons, encrypted Travis variables are not available to PRs, so we can't rely on
|
||||
them to implement security.
|
||||
|
||||
|
||||
## Implemented approach
|
||||
|
||||
|
||||
### In a nutshell
|
||||
The implemented approach can be broken up to the following sub-tasks:
|
||||
|
||||
1. Verify which PR the uploaded artifacts correspond to.
|
||||
2. Determine the author of the PR.
|
||||
3. Check whether the PR author is a member of some whitelisted GitHub team.
|
||||
4. Deploy the artifacts to the corresponding PR's directory.
|
||||
5. Prevent overwriting previously deployed artifacts (which ensures that the guarantees established
|
||||
during deployment will remain valid until the artifacts are removed).
|
||||
6. Prevent uploaded files from accessing anything outside their directory.
|
||||
|
||||
|
||||
### Implementation details
|
||||
This section describes how each of the aforementioned sub-tasks is accomplished:
|
||||
|
||||
1. **Verify which PR the uploaded artifacts correspond to.**
|
||||
|
||||
We are taking advantage of Travis' [JWT addon](https://docs.travis-ci.com/user/jwt). By sharing
|
||||
a secret between Travis (which keeps it private but uses it to sign a JWT) and the server (which
|
||||
uses it to verify the authenticity of the JWT), we can accomplish the following:
|
||||
a. Verify that the upload request comes from Travis.
|
||||
b. Determine the PR that these artifacts correspond to (since Travis puts that information into
|
||||
the JWT, without the PR author being able to modify it).
|
||||
|
||||
_Note:_
|
||||
_There are currently certain limitation in the implementation of the JWT addon._
|
||||
_See the next section for more details._
|
||||
|
||||
2. **Determine the author of the PR.**
|
||||
|
||||
Once we have securely associated the uploaded artifaacts to a PR, we retrieve the PR's metadata -
|
||||
including the author's username - using the [GitHub API](https://developer.github.com/v3/).
|
||||
To avoid rate-limit restrictions, we use a Personal Access Token (issued by
|
||||
[@mary-poppins](https://github.com/mary-poppins)).
|
||||
|
||||
3. **Check whether the PR author is a member of some whitelisted GitHub team.**
|
||||
|
||||
Again using the GitHub API, we can verify 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 token by @mary-poppins.
|
||||
|
||||
4. **Deploy the artifacts to the corresponding PR's directory.**
|
||||
|
||||
With the preceeding steps, we have verified that the uploaded artifacts have been uploaded by
|
||||
Travis and correspond to a PR whose author is a member of a trusted team. 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.
|
||||
|
||||
5. **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
|
||||
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._
|
||||
|
||||
6. **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
|
||||
outside of the directory where the build artifacts are stored.
|
||||
|
||||
|
||||
## Assumptions / Things to keep in mind
|
||||
|
||||
- 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.
|
||||
|
||||
- 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
|
||||
corresponding PR's author on the preview server for as long as the token is valid (currently 90
|
||||
mins). Because of this, the value of the `PREVIEW_DEPLOYMENT_TOKEN` should not be made publicly
|
||||
accessible (e.g. by printing it on the Travis job log).
|
||||
|
||||
- Travis does only allow specific whitelisted property names to be used with the JWT addon. The only
|
||||
known such property at the time is `SAUCE_ACCESS_KEY` (used for integration with SauceLabs). In
|
||||
order to be able to actually use the JWT addon we had to name the encrypted variable
|
||||
`SAUCE_ACCESS_KEY` (which we later re-assign to `NGBUILDS_IO_KEY`).
|
@ -1,20 +0,0 @@
|
||||
# VM setup - Attach persistent disk
|
||||
|
||||
|
||||
## Create `aio-builds` persistent disk (if not already exists)
|
||||
- Follow instructions [here](https://cloud.google.com/compute/docs/disks/add-persistent-disk#create_disk).
|
||||
- `sudo mkfs.ext4 -F -E lazy_itable_init=0,lazy_journal_init=0,discard /dev/disk/by-id/google-aio-builds`
|
||||
|
||||
|
||||
## Mount disk
|
||||
- `sudo mkdir -p /mnt/disks/aio-builds`
|
||||
- `sudo mount -o discard,defaults /dev/disk/by-id/google-aio-builds /mnt/disks/aio-builds`
|
||||
- `sudo chmod a+w /mnt/disks/aio-builds`
|
||||
|
||||
|
||||
## Mount disk on boot
|
||||
- Run:
|
||||
```
|
||||
echo UUID=`sudo blkid -s UUID -o value /dev/disk/by-id/google-aio-builds` \
|
||||
/mnt/disks/aio-builds ext4 discard,defaults,nofail 0 2 | sudo tee -a /etc/fstab
|
||||
```
|
@ -1,32 +0,0 @@
|
||||
# VM setup - Create docker image
|
||||
|
||||
|
||||
## Checkout repository
|
||||
- `git clone <repo-url>`
|
||||
|
||||
|
||||
## Build docker image
|
||||
- `<aio-builds-setup-dir>/scripts/build.sh [<name>[:<tag>] [--build-arg <NAME>=<value> ...]]`
|
||||
- You can overwrite the default environment variables inside the image, by passing new values using
|
||||
`--build-arg`.
|
||||
|
||||
**Note:** The build script has to execute docker commands with `sudo`.
|
||||
|
||||
|
||||
## Example
|
||||
The following commands would create a docker image from GitHub repo `foo/bar` to be deployed on the
|
||||
`foobar-builds.io` domain and accepting PR deployments from authors that are members of the
|
||||
`bar-core` and `bar-docs-authors` teams of organization `foo`:
|
||||
|
||||
- `git clone https://github.com/foo/bar.git foobar`
|
||||
- Run:
|
||||
```
|
||||
./foobar/aio-builds-setup/scripts/build.sh foobar-builds \
|
||||
--build-arg AIO_REPO_SLUG=foo/bar \
|
||||
--build-arg AIO_DOMAIN_NAME=foobar-builds.io \
|
||||
--build-arg AIO_GITHUB_ORGANIZATION=foo \
|
||||
--build-arg AIO_GITHUB_TEMA_SLUGS=bar-core,bar-docs-authors
|
||||
```
|
||||
|
||||
A full list of the available environment variables can be found
|
||||
[here](image-config--environment-variables.md).
|
@ -1,74 +0,0 @@
|
||||
# VM setup - Create host directories and files
|
||||
|
||||
|
||||
## Create directory with secrets
|
||||
For security reasons, sensitive info (such as tokens and passwords) are not hardcoded into the
|
||||
docker image, nor passed as environment variables at runtime. They are passed to the docker
|
||||
container from the host VM as files inside a directory. Each file's name is the name of the variable
|
||||
and the file content is the value. These are read from inside the running container when necessary.
|
||||
|
||||
More info on how to create `secrets` directory and files can be found
|
||||
[here](vm-setup--set-up-secrets.md).
|
||||
|
||||
|
||||
## Create directory for build artifacts
|
||||
The uploaded build artifacts should be kept on a directory outside the docker container, so it is
|
||||
easier to replace the container without losing the uploaded builds. For portability across VMs a
|
||||
persistent disk can be used (as described [here](vm-setup--attach-persistent-disk.md)).
|
||||
|
||||
**Note:** The directories created inside that directory will be owned by user `www-data`.
|
||||
|
||||
|
||||
## Create SSL certificates (Optional for dev)
|
||||
The host VM can attach a directory containing the SSL certificate and key to be used by the nginx
|
||||
server for serving the uploaded build artifacts. More info on how to attach the directory when
|
||||
starting the container can be found [here](vm-setup--start-docker-container.md).
|
||||
|
||||
In order for the container to be able to find the certificate and key, they should be named
|
||||
`<DOMAIN_NAME>.crt` and `<DOMAIN_NAME>.key` respectively. For example, for a domain name
|
||||
`ngbuild.io`, nginx will look for files `ngbuilds.io.crt` and `ngbuilds.io.key`. More info on how to
|
||||
specify the domain name see [here](vm-setup--create-docker-image.md).
|
||||
|
||||
If no directory is attached, nginx will use an internal self-signed certificate. This is convenient
|
||||
during development, but is not suitable for production.
|
||||
|
||||
**Note:**
|
||||
Since nginx needs to be able to serve requests for both the main domain as well as any subdomain
|
||||
(e.g. `ngbuilds.io/` and `foo-bar.ngbuilds.io/`), the provided certificate needs to be a wildcard
|
||||
certificate covering both the domain and subdomains.
|
||||
|
||||
|
||||
## Create directory for logs (Optional)
|
||||
Optionally, a logs directory can pe passed to the docker container for storing non-system-related
|
||||
logs. If not provided, the logs are kept locally on the container and will be lost whenever the
|
||||
container is replaced (e.g. when updating to use a newer version of the docker image).
|
||||
|
||||
The following files log files are kept in this directory:
|
||||
|
||||
- `clean-up.log`:
|
||||
Output of the `aio-clean-up` command, run as a cronjob for cleaning up the build artifacts of
|
||||
closed PRs.
|
||||
|
||||
- `init.log`:
|
||||
Output of the `aio-init` command, run (by default) when starting the container.
|
||||
|
||||
- `nginx/{access,error}.log`:
|
||||
The access and error logs produced by the nginx server while serving "production" files.
|
||||
|
||||
- `nginx-test/{access,error}.log`:
|
||||
The access and error logs produced by the nginx server while serving "test" files. This is only
|
||||
used when running tests locally from inside the container, e.g. with the `aio-verify-setup`
|
||||
command. (See [here](overview--scripts-and-commands.md) for more info.)
|
||||
|
||||
- `upload-server-{prod,test,verify-setup}-*.log`:
|
||||
The logs produced by the Node.js upload-server while serving either:
|
||||
- `-prod`: "Production" files (g.g during normal operation).
|
||||
- `-test`: "Test" files (e.g. when a test instance is started with the `aio-upload-server-test`
|
||||
command).
|
||||
- `-verify-setup`: "Test" files, but while running `aio-verify-setup`.
|
||||
|
||||
(See [here](overview--scripts-and-commands.md) for more info the commands mentioned above.)
|
||||
|
||||
- `verify-setup.log`:
|
||||
The output of the `aio-verify-setup` command (e.g. Jasmine output), except for upload-server
|
||||
output which is logged to `upload-server-verify-setup-*.log` (see above).
|
@ -1,35 +0,0 @@
|
||||
# VM Setup - Set up docker
|
||||
|
||||
|
||||
## Install docker
|
||||
|
||||
_Debian (jessie):_
|
||||
- `sudo apt-get update`
|
||||
- `sudo apt-get install -y apt-transport-https ca-certificates curl git software-properties-common`
|
||||
- `curl -fsSL https://apt.dockerproject.org/gpg | sudo apt-key add -`
|
||||
- `apt-key fingerprint 58118E89F3A912897C070ADBF76221572C52609D`
|
||||
- `sudo add-apt-repository "deb https://apt.dockerproject.org/repo/ debian-$(lsb_release -cs) main"`
|
||||
- `sudo apt-get update`
|
||||
- `sudo apt-get -y install docker-engine`
|
||||
|
||||
_Ubuntu (16.04):_
|
||||
- `sudo apt-get update`
|
||||
- `sudo apt-get install -y curl git linux-image-extra-$(uname -r) linux-image-extra-virtual`
|
||||
- `sudo apt-get install -y apt-transport-https ca-certificates`
|
||||
- `curl -fsSL https://yum.dockerproject.org/gpg | sudo apt-key add -`
|
||||
- `apt-key fingerprint 58118E89F3A912897C070ADBF76221572C52609D`
|
||||
- `sudo add-apt-repository "deb https://apt.dockerproject.org/repo/ ubuntu-$(lsb_release -cs) main"`
|
||||
- `sudo apt-get update`
|
||||
- `sudo apt-get -y install docker-engine`
|
||||
|
||||
|
||||
## Start the docker
|
||||
- `sudo service docker start`
|
||||
|
||||
|
||||
## Test docker
|
||||
- `sudo docker run hello-world`
|
||||
|
||||
|
||||
## Start docker on boot
|
||||
- `sudo systemctl enable docker`
|
@ -1,52 +0,0 @@
|
||||
# VM Setup - Set up secrets
|
||||
|
||||
|
||||
## Overview
|
||||
|
||||
Necessary secrets:
|
||||
|
||||
1. `GITHUB_TOKEN`
|
||||
- Used for:
|
||||
- Retrieving open PRs without rate-limiting.
|
||||
- Retrieving PR author.
|
||||
- Retrieving members of the `angular-core` team.
|
||||
- Posting comments with preview links on PRs.
|
||||
|
||||
2. `PREVIEW_DEPLOYMENT_TOKEN`
|
||||
- Used for:
|
||||
- Decoding the JWT tokens received with `/create-build` requests.
|
||||
|
||||
**Note:**
|
||||
`TEST_GITHUB_TOKEN` and `TEST_PREVIEW_DEPLOYMENT_TOKEN` can also be created similar to their
|
||||
non-TEST counterparts and they will be loaded when running `aio-verify-setup`, but it is currently
|
||||
not clear if/how they can be used in tests.
|
||||
|
||||
|
||||
## Create secrets
|
||||
|
||||
1. `GITHUB_TOKEN`
|
||||
- Visit https://github.com/settings/tokens.
|
||||
- Generate new token with the `public_repo` scope.
|
||||
|
||||
2. `PREVIEW_DEPLOYMENT_TOKEN`
|
||||
- Just generate a hard-to-guess character sequence.
|
||||
- Add it to `.travis.yml` under `addons -> jwt -> secure`.
|
||||
Can be added automatically with: `travis encrypt --add addons.jwt PREVIEW_DEPLOYMENT_TOKEN=<access-key>`
|
||||
|
||||
**Note:**
|
||||
Due to [travis-ci/travis-ci#7223](https://github.com/travis-ci/travis-ci/issues/7223) it is not
|
||||
currently possible to use the JWT addon (as described above) for anything other than the
|
||||
`SAUCE_ACCESS_KEY` variable. You can get creative, though...
|
||||
|
||||
**WARNING**
|
||||
TO avoid arbitrary uploads, make sure the `PREVIEW_DEPLOYMENT_TOKEN` is NOT printed in the Travis log.
|
||||
|
||||
|
||||
## Save secrets on the VM
|
||||
|
||||
- `sudo mkdir /aio-secrets`
|
||||
- `sudo touch /aio-secrets/GITHUB_TOKEN`
|
||||
- Insert `<github-token>` into `/aio-secrets/GITHUB_TOKEN`.
|
||||
- `sudo touch /aio-secrets/PREVIEW_DEPLOYMENT_TOKEN`
|
||||
- Insert `<access-token>` into `/aio-secrets/PREVIEW_DEPLOYMENT_TOKEN`.
|
||||
- `sudo chmod 400 /aio-secrets/*`
|
@ -1,92 +0,0 @@
|
||||
# VM setup - Start docker container
|
||||
|
||||
|
||||
## The `docker run` command
|
||||
Once everything has been setup and configured, a docker container can be started with the following
|
||||
command:
|
||||
|
||||
```
|
||||
sudo docker run \
|
||||
-d \
|
||||
--dns 127.0.0.1 \
|
||||
--name <instance-name> \
|
||||
-p 80:80 \
|
||||
-p 443:443 \
|
||||
--restart unless-stopped \
|
||||
[-v <host-cert-dir>:/etc/ssl/localcerts:ro] \
|
||||
-v <host-secrets-dir>:/aio-secrets:ro \
|
||||
-v <host-builds-dir>:/var/www/aio-builds \
|
||||
[-v <host-logs-dir>:/var/log/aio] \
|
||||
<name>[:<tag>]
|
||||
```
|
||||
|
||||
Below is the same command with inline comments explaining each option. The aPI docs for `docker run`
|
||||
can be found [here](https://docs.docker.com/engine/reference/run/).
|
||||
|
||||
```
|
||||
sudo docker run \
|
||||
|
||||
# Start as a daemon.
|
||||
-d \
|
||||
|
||||
# Use the local DNS server.
|
||||
# (This is necessary for mapping internal URLs, e.g. for the Node.js upload-server.)
|
||||
--dns 127.0.0.1 \
|
||||
|
||||
# USe `<instance-name>` as an alias for the container.
|
||||
# Useful for running `docker` commands, e.g.: `docker stop <instance-name>`
|
||||
--name <instance-name> \
|
||||
|
||||
# Map ports of the hosr VM (left) to ports of the docker container (right)
|
||||
-p 80:80 \
|
||||
-p 443:443 \
|
||||
|
||||
# Automatically restart the container (unless it was explicitly stopped by the user).
|
||||
# (This ensures that the container will be automatically started on boot.)
|
||||
--restart unless-stopped \
|
||||
|
||||
# The directory the contains the SSL certificates.
|
||||
# (See [here](vm-setup--create-host-dirs-and-files.md) for more info.)
|
||||
# If not provided, the container will use self-signed certificates.
|
||||
[-v <host-cert-dir>:/etc/ssl/localcerts:ro] \
|
||||
|
||||
# The directory the contains the secrets (e.g. GitHub token, JWT secret, etc).
|
||||
# (See [here](vm-setup--set-up-secrets.md) for more info.)
|
||||
-v <host-secrets-dir>:/aio-secrets:ro \
|
||||
|
||||
# The uploaded build artifacts will stored to and served from this directory.
|
||||
# (If you are using a persistent disk - as described [here](vm-setup--attach-persistent-disk.md) -
|
||||
# this will be a directory inside the disk.)
|
||||
-v <host-builds-dir>:/var/www/aio-builds \
|
||||
|
||||
# The directory where the logs are being kept.
|
||||
# (See [here](vm-setup--create-host-dirs-and-files.md) for more info.)
|
||||
# If not provided, the logs will be kept inside the container, which means they will be lost
|
||||
# whenever a new container is created.
|
||||
[-v <host-logs-dir>:/var/log/aio] \
|
||||
|
||||
# The name of the docker image to use (and an optional tag; defaults to `latest`).
|
||||
# (See [here](vm-setup--create-docker-image.md) for instructions on how to create the iamge.)
|
||||
<name>[:<tag>]
|
||||
```
|
||||
|
||||
|
||||
## Example
|
||||
The following command would start a docker container based on the previously created `foobar-builds`
|
||||
docker image, alias it as 'foobar-builds-1' and map predefined directories on the host VM to be used
|
||||
by the container for accesing secrets and SSL certificates and keeping the build artifacts and logs.
|
||||
|
||||
```
|
||||
sudo docker run \
|
||||
-d \
|
||||
--dns 127.0.0.1 \
|
||||
--name foobar-builds-1 \
|
||||
-p 80:80 \
|
||||
-p 443:443 \
|
||||
--restart unless-stopped \
|
||||
-v /etc/ssl/localcerts:/etc/ssl/localcerts:ro \
|
||||
-v /foobar-secrets:/aio-secrets:ro \
|
||||
-v /mnt/disks/foobar-builds:/var/www/aio-builds \
|
||||
-v /foobar-logs:/var/log/aio \
|
||||
foobar-builds
|
||||
```
|
@ -1,16 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -eux -o pipefail
|
||||
|
||||
# Set up env
|
||||
source "`dirname $0`/env.sh"
|
||||
readonly defaultImageNameAndTag="aio-builds:latest"
|
||||
|
||||
# Build `scripts-js/`
|
||||
cd "$SCRIPTS_JS_DIR"
|
||||
yarn install
|
||||
yarn run build
|
||||
cd -
|
||||
|
||||
# Create docker image
|
||||
readonly nameAndOptionalTag=${1:-$defaultImageNameAndTag}
|
||||
sudo docker build --tag $nameAndOptionalTag ${@:2} $DOCKERBUILD_DIR
|
@ -1,5 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
readonly THIS_DIR=$(cd $(dirname $0); pwd)
|
||||
readonly DOCKERBUILD_DIR="$THIS_DIR/../dockerbuild"
|
||||
readonly SCRIPTS_JS_DIR="$DOCKERBUILD_DIR/scripts-js"
|
@ -1,11 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -eux -o pipefail
|
||||
|
||||
# Set up env
|
||||
source "`dirname $0`/env.sh"
|
||||
|
||||
# Test `scripts-js/`
|
||||
cd "$SCRIPTS_JS_DIR"
|
||||
yarn install
|
||||
yarn test
|
||||
cd -
|
@ -1,13 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -eux -o pipefail
|
||||
|
||||
# Set up env
|
||||
source "`dirname $0`/env.sh"
|
||||
|
||||
# Preverify PR
|
||||
AIO_GITHUB_ORGANIZATION="angular" \
|
||||
AIO_GITHUB_TEAM_SLUGS="angular-core,aio-contributors" \
|
||||
AIO_GITHUB_TOKEN=$(echo ${GITHUB_TEAM_MEMBERSHIP_CHECK_KEY} | rev) \
|
||||
AIO_REPO_SLUG=$TRAVIS_REPO_SLUG \
|
||||
AIO_PREVERIFY_PR=$TRAVIS_PULL_REQUEST \
|
||||
node "$SCRIPTS_JS_DIR/dist/lib/upload-server/index-preverify-pr"
|
17
aio/content/examples/.gitignore
vendored
17
aio/content/examples/.gitignore
vendored
@ -1,17 +0,0 @@
|
||||
# _boilerplate files
|
||||
!_boilerplate/*
|
||||
*/*/src/styles.css
|
||||
*/*/src/systemjs.config.js
|
||||
*/*/src/tsconfig.json
|
||||
*/*/bs-config.e2e.json
|
||||
*/*/bs-config.json
|
||||
*/*/package.json
|
||||
*/*/tslint.json
|
||||
|
||||
# example files
|
||||
_test-output
|
||||
protractor-helpers.js
|
||||
*/e2e-spec.js
|
||||
**/ts/**/*.js
|
||||
**/js-es6*/**/*.js
|
||||
**/ts-snippets/**/*.js
|
@ -1,14 +0,0 @@
|
||||
{
|
||||
"open": false,
|
||||
"logLevel": "silent",
|
||||
"port": 8080,
|
||||
"server": {
|
||||
"baseDir": "src",
|
||||
"routes": {
|
||||
"/node_modules": "node_modules"
|
||||
},
|
||||
"middleware": {
|
||||
"0": null
|
||||
}
|
||||
}
|
||||
}
|
@ -1,8 +0,0 @@
|
||||
{
|
||||
"server": {
|
||||
"baseDir": "src",
|
||||
"routes": {
|
||||
"/node_modules": "node_modules"
|
||||
}
|
||||
}
|
||||
}
|
@ -1,4 +0,0 @@
|
||||
{
|
||||
"build": "build",
|
||||
"run": "serve"
|
||||
}
|
@ -1,44 +0,0 @@
|
||||
{
|
||||
"name": "angular-examples",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"description": "Example package.json, only contains needed scripts for examples. See _examples/package.json for master package.json.",
|
||||
"scripts": {
|
||||
"build": "tsc -p src/",
|
||||
"build:watch": "tsc -p src/ -w",
|
||||
"build:e2e": "tsc -p e2e/",
|
||||
"serve": "lite-server -c=bs-config.json",
|
||||
"serve:e2e": "lite-server -c=bs-config.e2e.json",
|
||||
"prestart": "npm run build",
|
||||
"start": "concurrently \"npm run build:watch\" \"npm run serve\"",
|
||||
"pree2e": "webdriver-manager update && npm run build:e2e",
|
||||
"e2e": "concurrently \"npm run serve:e2e\" \"npm run protractor\" --kill-others --success first",
|
||||
"protractor": "protractor protractor.config.js",
|
||||
"pretest": "npm run build",
|
||||
"test": "concurrently \"npm run build:watch\" \"karma start karma.conf.js\"",
|
||||
"pretest:once": "npm run build",
|
||||
"test:once": "karma start karma.conf.js --single-run",
|
||||
"lint": "tslint ./src/**/*.ts -t verbose",
|
||||
|
||||
"build:upgrade": "tsc",
|
||||
"serve:upgrade": "http-server",
|
||||
"build:cli": "ng build --no-progress",
|
||||
"serve:cli": "http-server dist/",
|
||||
"build:aot": "ngc -p tsconfig-aot.json && rollup -c rollup-config.js",
|
||||
"serve:aot": "lite-server -c bs-config.aot.json",
|
||||
"start:webpack": "webpack-dev-server --inline --progress --port 8080",
|
||||
"test:webpack": "karma start karma.webpack.conf.js",
|
||||
"build:webpack": "rimraf dist && webpack --config config/webpack.prod.js --bail",
|
||||
"build:babel": "babel src -d src --extensions \".es6\" --source-maps",
|
||||
"copy-dist-files": "node ./copy-dist-files.js",
|
||||
"i18n": "ng-xi18n"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "MIT",
|
||||
"dependencies": {},
|
||||
"devDependencies": {
|
||||
"angular-cli": "^1.0.0-beta.26"
|
||||
},
|
||||
"repository": {}
|
||||
}
|
@ -1,10 +0,0 @@
|
||||
{
|
||||
"description": "QuickStart",
|
||||
"basePath": "src/",
|
||||
"files": [
|
||||
"app/app.component.ts",
|
||||
"index.html"
|
||||
],
|
||||
"open": "app/app.component.ts",
|
||||
"tags": ["quickstart"]
|
||||
}
|
@ -1,116 +0,0 @@
|
||||
/* #docregion , quickstart, toh */
|
||||
/* Master Styles */
|
||||
h1 {
|
||||
color: #369;
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
font-size: 250%;
|
||||
}
|
||||
h2, h3 {
|
||||
color: #444;
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
font-weight: lighter;
|
||||
}
|
||||
body {
|
||||
margin: 2em;
|
||||
}
|
||||
/* #enddocregion quickstart */
|
||||
body, input[text], button {
|
||||
color: #888;
|
||||
font-family: Cambria, Georgia;
|
||||
}
|
||||
/* #enddocregion toh */
|
||||
a {
|
||||
cursor: pointer;
|
||||
cursor: hand;
|
||||
}
|
||||
button {
|
||||
font-family: Arial;
|
||||
background-color: #eee;
|
||||
border: none;
|
||||
padding: 5px 10px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
cursor: hand;
|
||||
}
|
||||
button:hover {
|
||||
background-color: #cfd8dc;
|
||||
}
|
||||
button:disabled {
|
||||
background-color: #eee;
|
||||
color: #aaa;
|
||||
cursor: auto;
|
||||
}
|
||||
|
||||
/* Navigation link styles */
|
||||
nav a {
|
||||
padding: 5px 10px;
|
||||
text-decoration: none;
|
||||
margin-right: 10px;
|
||||
margin-top: 10px;
|
||||
display: inline-block;
|
||||
background-color: #eee;
|
||||
border-radius: 4px;
|
||||
}
|
||||
nav a:visited, a:link {
|
||||
color: #607D8B;
|
||||
}
|
||||
nav a:hover {
|
||||
color: #039be5;
|
||||
background-color: #CFD8DC;
|
||||
}
|
||||
nav a.active {
|
||||
color: #039be5;
|
||||
}
|
||||
|
||||
/* items class */
|
||||
.items {
|
||||
margin: 0 0 2em 0;
|
||||
list-style-type: none;
|
||||
padding: 0;
|
||||
width: 24em;
|
||||
}
|
||||
.items li {
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
left: 0;
|
||||
background-color: #EEE;
|
||||
margin: .5em;
|
||||
padding: .3em 0;
|
||||
height: 1.6em;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.items li:hover {
|
||||
color: #607D8B;
|
||||
background-color: #DDD;
|
||||
left: .1em;
|
||||
}
|
||||
.items li.selected {
|
||||
background-color: #CFD8DC;
|
||||
color: white;
|
||||
}
|
||||
.items li.selected:hover {
|
||||
background-color: #BBD8DC;
|
||||
}
|
||||
.items .text {
|
||||
position: relative;
|
||||
top: -3px;
|
||||
}
|
||||
.items .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;
|
||||
}
|
||||
/* #docregion toh */
|
||||
/* everywhere else */
|
||||
* {
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
}
|
@ -1,42 +0,0 @@
|
||||
/**
|
||||
* System configuration for Angular samples
|
||||
* Adjust as necessary for your application needs.
|
||||
*/
|
||||
(function (global) {
|
||||
System.config({
|
||||
paths: {
|
||||
// paths serve as alias
|
||||
'npm:': 'node_modules/'
|
||||
},
|
||||
// map tells the System loader where to look for things
|
||||
map: {
|
||||
// our app is within the app folder
|
||||
app: 'app',
|
||||
|
||||
// angular bundles
|
||||
'@angular/core': 'npm:@angular/core/bundles/core.umd.js',
|
||||
'@angular/common': 'npm:@angular/common/bundles/common.umd.js',
|
||||
'@angular/compiler': 'npm:@angular/compiler/bundles/compiler.umd.js',
|
||||
'@angular/platform-browser': 'npm:@angular/platform-browser/bundles/platform-browser.umd.js',
|
||||
'@angular/platform-browser-dynamic': 'npm:@angular/platform-browser-dynamic/bundles/platform-browser-dynamic.umd.js',
|
||||
'@angular/http': 'npm:@angular/http/bundles/http.umd.js',
|
||||
'@angular/router': 'npm:@angular/router/bundles/router.umd.js',
|
||||
'@angular/router/upgrade': 'npm:@angular/router/bundles/router-upgrade.umd.js',
|
||||
'@angular/forms': 'npm:@angular/forms/bundles/forms.umd.js',
|
||||
|
||||
// other libraries
|
||||
'rxjs': 'npm:rxjs',
|
||||
'angular-in-memory-web-api': 'npm:angular-in-memory-web-api/bundles/in-memory-web-api.umd.js'
|
||||
},
|
||||
// packages tells the System loader how to load when no filename and/or no extension
|
||||
packages: {
|
||||
app: {
|
||||
main: './main.js',
|
||||
defaultExtension: 'js'
|
||||
},
|
||||
rxjs: {
|
||||
defaultExtension: 'js'
|
||||
}
|
||||
}
|
||||
});
|
||||
})(this);
|
@ -1,88 +0,0 @@
|
||||
/**
|
||||
* WEB VERSION FOR CURRENT ANGULAR BUILD
|
||||
* (based on systemjs.config.js in angular.io)
|
||||
* System configuration for Angular samples
|
||||
* Adjust as necessary for your application needs.
|
||||
*
|
||||
* UNTESTED !
|
||||
*/
|
||||
(function (global) {
|
||||
System.config({
|
||||
// DEMO ONLY! REAL CODE SHOULD NOT TRANSPILE IN THE BROWSER
|
||||
transpiler: 'ts',
|
||||
typescriptOptions: {
|
||||
// Copy of compiler options in standard tsconfig.json
|
||||
"target": "es5",
|
||||
"module": "commonjs",
|
||||
"moduleResolution": "node",
|
||||
"sourceMap": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true,
|
||||
"lib": ["es2015", "dom"],
|
||||
"noImplicitAny": true,
|
||||
"suppressImplicitAnyIndexErrors": true
|
||||
},
|
||||
meta: {
|
||||
'typescript': {
|
||||
"exports": "ts"
|
||||
}
|
||||
},
|
||||
paths: {
|
||||
// paths serve as alias
|
||||
'npm:': 'https://unpkg.com/',
|
||||
'ng:': 'https://cdn.rawgit.com/angular/'
|
||||
},
|
||||
// map tells the System loader where to look for things
|
||||
map: {
|
||||
// our app is within the app folder
|
||||
app: 'app',
|
||||
|
||||
// angular bundles
|
||||
'@angular/core': 'ng:core-builds/master/bundles/core.umd.js',
|
||||
'@angular/common': 'ng:common-builds/master/bundles/common.umd.js',
|
||||
'@angular/compiler': 'ng:compiler-builds/master/bundles/compiler.umd.js',
|
||||
'@angular/platform-browser': 'ng:platform-browser-builds/master/bundles/platform-browser.umd.js',
|
||||
'@angular/platform-browser-dynamic': 'ng:platform-browser-dynamic-builds/master/bundles/platform-browser-dynamic.umd.js',
|
||||
'@angular/http': 'ng:http-builds/master/bundles/http.umd.js',
|
||||
'@angular/router': 'ng:router-builds/master/bundles/router.umd.js',
|
||||
'@angular/router/upgrade': 'ng:router-builds/master/bundles/router-upgrade.umd.js',
|
||||
'@angular/forms': 'ng:forms-builds/master/bundles/forms.umd.js',
|
||||
'@angular/upgrade': 'ng:upgrade-builds/master/bundles/upgrade.umd.js',
|
||||
'@angular/upgrade/static': 'ng:upgrade-builds/master/bundles/upgrade-static.umd.js',
|
||||
|
||||
// angular testing umd bundles (overwrite the shim mappings)
|
||||
'@angular/core/testing': 'ng:core-builds/master/bundles/core-testing.umd.js',
|
||||
'@angular/common/testing': 'ng:common-builds/master/bundles/common-testing.umd.js',
|
||||
'@angular/compiler/testing': 'ng:compiler-builds/master/bundles/compiler-testing.umd.js',
|
||||
'@angular/platform-browser/testing': 'ng:platform-browser-builds/master/bundles/platform-browser-testing.umd.js',
|
||||
'@angular/platform-browser-dynamic/testing': 'ng:platform-browser-dynamic-builds/master/bundles/platform-browser-dynamic-testing.umd.js',
|
||||
'@angular/http/testing': 'ng:http-builds/master/bundles/http-testing.umd.js',
|
||||
'@angular/router/testing': 'ng:router-builds/master/bundles/router-testing.umd.js',
|
||||
'@angular/forms/testing': 'ng:forms-builds/master/bundles/forms-testing.umd.js',
|
||||
|
||||
// other libraries
|
||||
'rxjs': 'npm:rxjs@5.0.1',
|
||||
'angular-in-memory-web-api': 'npm:angular-in-memory-web-api/bundles/in-memory-web-api.umd.js',
|
||||
'ts': 'npm:plugin-typescript@5.2.7/lib/plugin.js',
|
||||
'typescript': 'npm:typescript@2.0.10/lib/typescript.js',
|
||||
|
||||
},
|
||||
// packages tells the System loader how to load when no filename and/or no extension
|
||||
packages: {
|
||||
app: {
|
||||
main: './main.ts',
|
||||
defaultExtension: 'ts'
|
||||
},
|
||||
rxjs: {
|
||||
defaultExtension: 'js'
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
})(this);
|
||||
|
||||
/*
|
||||
Copyright 2016 Google Inc. All Rights Reserved.
|
||||
Use of this source code is governed by an MIT-style license that
|
||||
can be found in the LICENSE file at http://angular.io/license
|
||||
*/
|
@ -1,75 +0,0 @@
|
||||
/**
|
||||
* WEB ANGULAR VERSION
|
||||
* (based on systemjs.config.js in angular.io)
|
||||
* System configuration for Angular samples
|
||||
* Adjust as necessary for your application needs.
|
||||
*/
|
||||
(function (global) {
|
||||
System.config({
|
||||
// DEMO ONLY! REAL CODE SHOULD NOT TRANSPILE IN THE BROWSER
|
||||
transpiler: 'ts',
|
||||
typescriptOptions: {
|
||||
// Copy of compiler options in standard tsconfig.json
|
||||
"target": "es5",
|
||||
"module": "commonjs",
|
||||
"moduleResolution": "node",
|
||||
"sourceMap": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true,
|
||||
"lib": ["es2015", "dom"],
|
||||
"noImplicitAny": true,
|
||||
"suppressImplicitAnyIndexErrors": true
|
||||
},
|
||||
meta: {
|
||||
'typescript': {
|
||||
"exports": "ts"
|
||||
}
|
||||
},
|
||||
paths: {
|
||||
// paths serve as alias
|
||||
'npm:': 'https://unpkg.com/'
|
||||
},
|
||||
// map tells the System loader where to look for things
|
||||
map: {
|
||||
// our app is within the app folder
|
||||
app: 'app',
|
||||
|
||||
// angular bundles
|
||||
'@angular/core': 'npm:@angular/core/bundles/core.umd.js',
|
||||
'@angular/common': 'npm:@angular/common/bundles/common.umd.js',
|
||||
'@angular/compiler': 'npm:@angular/compiler/bundles/compiler.umd.js',
|
||||
'@angular/platform-browser': 'npm:@angular/platform-browser/bundles/platform-browser.umd.js',
|
||||
'@angular/platform-browser-dynamic': 'npm:@angular/platform-browser-dynamic/bundles/platform-browser-dynamic.umd.js',
|
||||
'@angular/http': 'npm:@angular/http/bundles/http.umd.js',
|
||||
'@angular/router': 'npm:@angular/router/bundles/router.umd.js',
|
||||
'@angular/router/upgrade': 'npm:@angular/router/bundles/router-upgrade.umd.js',
|
||||
'@angular/forms': 'npm:@angular/forms/bundles/forms.umd.js',
|
||||
'@angular/upgrade': 'npm:@angular/upgrade/bundles/upgrade.umd.js',
|
||||
'@angular/upgrade/static': 'npm:@angular/upgrade/bundles/upgrade-static.umd.js',
|
||||
|
||||
// other libraries
|
||||
'rxjs': 'npm:rxjs@5.0.1',
|
||||
'angular-in-memory-web-api': 'npm:angular-in-memory-web-api/bundles/in-memory-web-api.umd.js',
|
||||
'ts': 'npm:plugin-typescript@5.2.7/lib/plugin.js',
|
||||
'typescript': 'npm:typescript@2.0.10/lib/typescript.js',
|
||||
|
||||
},
|
||||
// packages tells the System loader how to load when no filename and/or no extension
|
||||
packages: {
|
||||
app: {
|
||||
main: './main.ts',
|
||||
defaultExtension: 'ts'
|
||||
},
|
||||
rxjs: {
|
||||
defaultExtension: 'js'
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
})(this);
|
||||
|
||||
/*
|
||||
Copyright 2016 Google Inc. All Rights Reserved.
|
||||
Use of this source code is governed by an MIT-style license that
|
||||
can be found in the LICENSE file at http://angular.io/license
|
||||
*/
|
@ -1,21 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"module": "commonjs",
|
||||
"moduleResolution": "node",
|
||||
"sourceMap": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true,
|
||||
"lib": [ "es2015", "dom" ],
|
||||
"noImplicitAny": true,
|
||||
"suppressImplicitAnyIndexErrors": true,
|
||||
"typeRoots": [
|
||||
"../../../node_modules/@types/"
|
||||
]
|
||||
},
|
||||
"compileOnSave": true,
|
||||
"exclude": [
|
||||
"node_modules/*",
|
||||
"**/*-aot.ts"
|
||||
]
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user