Compare commits

..

3 Commits

Author SHA1 Message Date
185c679563 docs(changelog): update change log to beta.5 2016-02-10 16:16:05 -08:00
b3f4e05b57 chore(release): bump version to beta.5 2016-02-10 16:14:21 -08:00
abd1e6e680 fix(release): need to depend on latest rxjs and zone.js
The version in our package.json gets copied to the one we publish, and users need the latest of these.
2016-02-10 16:13:02 -08:00
5421 changed files with 148945 additions and 350930 deletions

6
.gitattributes vendored
View File

@ -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

View File

@ -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` =

View File

@ -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
View File

@ -1,9 +1,30 @@
.DS_STORE
# Dont 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

2
.nvmrc
View File

@ -1 +1 @@
6.9.5
5.4.1

View File

@ -1,257 +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
- tbosch #secondary
- vicb
- IgorMinar #fallback
- mhevery #fallback
router:
conditions:
files:
- "packages/router/*"
users:
- jasonaden
- vicb
- 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

View File

@ -1,75 +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: "aCdHveZuY8AT4Jr1JoJB4LxZsnGWRe/KseZh1YXYe5UtufFCtTVHvUcLn0j2aLBF0KpdyS+hWf0i4np9jthKu2xPKriefoPgCMpisYeC0MFkwbmv+XlgkUbgkgVZMGiVyX7DCYXVahxIoOUjVMEDCbNiHTIrfEuyq24U3ok2tHc="
# 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
- CI_MODE=aio_e2e
- 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: "CI_MODE=aio_e2e"
- 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=

File diff suppressed because it is too large Load Diff

34
COMMITTER.md Normal file
View 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)

View File

@ -1,6 +1,6 @@
# Contributing to Angular
# Contributing to Angular 2
We would love for you to contribute to Angular and help make it even better than it is
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)
@ -17,27 +17,19 @@ Help us keep Angular open and inclusive. Please read and follow our [Code of Con
## <a name="question"></a> Got a Question or Problem?
Please, do not open issues for the general support questions as we want to keep GitHub issues for bug reports and feature requests. You've got much better chances of getting your question answered on [StackOverflow](https://stackoverflow.com/questions/tagged/angular) where the questions should be tagged with tag `angular`.
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].
StackOverflow is a much better place to ask questions since:
- there are thousands of people willing to help on StackOverflow
- questions and answers stay available for public viewing so your question / answer might help someone else
- StackOverflow's voting system assures that the best answers are prominently visible.
To save your and our time we will be systematically closing all the issues that are requests for general support and redirecting people to StackOverflow.
If you would like to chat about the question in real-time, you can reach out via [our gitter channel][gitter].
## <a name="issue"></a> Found a Bug?
If you find a bug in the source code, you can help us by
## <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> Missing a Feature?
You can *request* a new feature by [submitting an issue](#submit-issue) to our GitHub
Repository. 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.
## <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
@ -48,22 +40,24 @@ and help you to craft the change so that it is successfully accepted into the 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.
Before you submit an issue, please search the issue tracker, maybe an issue for your problem already exists and the discussion might inform you of workarounds readily available.
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:
We want to fix all the issues as soon as possible, but before fixing a bug we need to reproduce and confirm it. In order to reproduce bugs we will systematically ask you to provide a minimal reproduction scenario using http://plnkr.co. Having a live, reproducible scenario gives us wealth of important information without going back & forth to you with additional questions like:
* **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)
- version of Angular used
- 3rd-party libraries and their versions
- and most importantly - a use-case that fails
A minimal reproduce scenario using http://plnkr.co/ allows us to quickly confirm a bug (or point out coding problem) as well as confirm that we are fixing the right problem. If plunker is not a suitable way to demonstrate the problem (for example for issues related to our npm packaging), please create a standalone git repository demonstrating the problem.
We will be insisting on a minimal reproduce scenario in order to save maintainers time and ultimately be able to fix more bugs. Interestingly, from our experience users often find coding problems themselves while preparing a minimal plunk. We understand that sometimes it might be hard to extract essentials bits of code from a larger code-base but we really need to isolate the problem before we can fix it.
Unfortunately we are not able to investigate / fix bugs without a minimal reproduction, so if we don't hear back from you we are going to close an issue that don't have enough info to be reproduced.
You can file new issues by filling out our [new issue form](https://github.com/angular/angular/issues/new).
You can file new issues by providing the above information [here](https://github.com/angular/angular/issues/new).
### <a name="submit-pr"></a> Submitting a Pull Request (PR)
@ -101,7 +95,7 @@ Before you submit your Pull Request (PR) consider the following guidelines:
* In GitHub, send a pull request to `angular:master`.
* If we suggest changes then:
* Make the required updates.
* Re-run the Angular test suites to ensure tests are still passing.
* 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
@ -147,7 +141,7 @@ To ensure consistency throughout the source code, keep these rules in mind as yo
* 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](docs/DEVELOPER.md#clang-format).
[DEVELOPER.md](DEVELOPER.md#clang-format).
## <a name="commit"></a> Commit Message Guidelines
@ -172,63 +166,27 @@ 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.
Footer should contain a [closing reference to an issue](https://help.github.com/articles/closing-issues-via-commit-messages/) if any.
Samples: (even more [samples](https://github.com/angular/angular/commits/master))
```
docs(changelog): update change log to beta.5
```
```
fix(release): need to depend on latest rxjs and zone.js
The version in our package.json gets copied to the one we publish, and users need the latest of these.
```
### 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:
* **build**: Changes that affect the build system or external dependencies (example scopes: gulp, broccoli, npm)
* **ci**: Changes to our CI configuration files and scripts (example scopes: Travis, Circle, BrowserStack, SauceLabs)
* **docs**: Documentation only changes
* **feat**: A new feature
* **fix**: A bug fix
* **perf**: A code change that improves performance
* **refactor**: A code change that neither fixes a bug nor adds a feature
* **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 should be the name of the npm package affected (as perceived by person reading changelog generated from commit messages.
The following is the list of supported scopes:
* **common**
* **compiler**
* **compiler-cli**
* **core**
* **forms**
* **http**
* **language-service**
* **platform-browser**
* **platform-browser-dynamic**
* **platform-server**
* **platform-webworker**
* **platform-webworker-dynamic**
* **router**
* **upgrade**
* **tsc-wrapped**
There is currently few exception to the "use package name" rule:
* **packaging**: used for changes that change the npm package layout in all of our packages, e.g. public path changes, package.json changes done to all packages, d.ts file/format changes, changes to bundles, etc.
* **changelog**: used for updating the release notes in CHANGELOG.md
* **aio**: used for docs-app (angular.io) related changes within the /aio directory of the repo
* none/empty string: useful for `style`, `test` and `refactor` changes that are done across all packages (e.g. `style: add missing semicolons`)
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:
@ -263,12 +221,12 @@ changes to be accepted, the CLA must be signed. It's a quick process, we promise
[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/docs/DEVELOPER.md
[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]: https://google.github.io/styleguide/jsguide.html
[jsfiddle]: http://jsfiddle.net
[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
[runnable]: http://runnable.com/
[stackoverflow]: http://stackoverflow.com/questions/tagged/angular

362
DEVELOPER.md Normal file
View 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 pubs 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.

View File

@ -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

View File

@ -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

View File

@ -1,18 +1,18 @@
[![Build Status](https://travis-ci.org/angular/angular.svg?branch=master)](https://travis-ci.org/angular/angular)
[![CircleCI](https://circleci.com/gh/angular/angular/tree/master.svg?style=shield)](https://circleci.com/gh/angular/angular/tree/master)
[![Build Status](https://travis-ci.org/angular/angular.svg?branch=master)](https://travis-ci.org/angular/angular)
[![Join the chat at https://gitter.im/angular/angular](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/angular/angular?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
[![Issue Stats](http://issuestats.com/github/angular/angular/badge/pr?style=flat)](http://issuestats.com/github/angular/angular)
[![Issue Stats](http://issuestats.com/github/angular/angular/badge/issue?style=flat)](http://issuestats.com/github/angular/angular)
[![npm version](https://badge.fury.io/js/%40angular%2Fcore.svg)](https://badge.fury.io/js/%40angular%2Fcore)
[![Issue Stats](http://issuestats.com/github/angular/angular/badge/pr)](http://issuestats.com/github/angular/angular)
[![Issue Stats](http://issuestats.com/github/angular/angular/badge/issue)](http://issuestats.com/github/angular/angular)
[![npm version](https://badge.fury.io/js/angular2.svg)](http://badge.fury.io/js/angular2)
[![Downloads](http://img.shields.io/npm/dm/angular2.svg)](https://npmjs.org/package/angular2)
[![Sauce Test Status](https://saucelabs.com/browser-matrix/angular2-ci.svg)](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
View File

@ -0,0 +1,4 @@
# Developer Tools for Angular 2
- [JavaScript](TOOLS_JS.md)
- [Dart](TOOLS_DART.md)

376
TOOLS_DART.md Normal file
View 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
View 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
View 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 &rarr; *tbosch*, *crossj*
* `comp: build/dev-productivity` -- build process, e.g. CLI and related tasks &rarr; *iminar*, *caitp*
* `comp: build/pipeline` -- build pipeline, e.g. ts2dart &rarr; *mprobst*, *alexeagle*
* `comp: core` -- general core Angular issues, not related to a sub-category (see below) &rarr;
*mhevery*
* `comp: core/animations` -- animations framework &rarr; *matsko*
* `comp: core/change_detection` -- change detection &rarr; *vsavkin*
* `comp: core/di` -- dependency injection &rarr; *vicb*, *rkirov*
* `comp: core/directives` -- directives
* `comp: core/forms` -- forms &rarr; *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 &rarr; *kegluneq*, *jakemac*
* `comp: data-access` -- &rarr; *jeffbcross*
* `comp: docs` -- API docs and doc generation &rarr; *naomiblack*, *petebacondarwin*
* `comp: material-components` -- Angular Material components built in Angular 2 &rarr; *jelbourn*
* `comp: router` -- Component Router &rarr; *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.

View File

@ -1,65 +0,0 @@
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"project": {
"name": "site"
},
"apps": [
{
"root": "src",
"outDir": "dist",
"assets": [
"assets",
"content",
"app/search/search-worker.js",
"favicon.ico",
"pwa-manifest.json"
],
"index": "index.html",
"main": "main.ts",
"polyfills": "polyfills.ts",
"test": "test.ts",
"tsconfig": "tsconfig.app.json",
"testTsconfig": "tsconfig.spec.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": [
{
"project": "src/tsconfig.app.json"
},
{
"project": "src/tsconfig.spec.json"
},
{
"project": "e2e/tsconfig.e2e.json"
}
],
"test": {
"karma": {
"config": "./karma.conf.js"
}
},
"defaults": {
"styleExt": "scss",
"component": {
"inlineStyle": true
}
}
}

View File

@ -1,5 +0,0 @@
{
"projects": {
"staging": "aio-staging"
}
}

45
aio/.gitignore vendored
View File

@ -1,45 +0,0 @@
# See http://help.github.com/ignore-files/ for more about ignoring files.
# compiled output
/dist
/out-tsc
/src/content
/tmp
# dependencies
/node_modules
# IDEs and editors
/.idea
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# IDE - VSCode
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
# misc
/.sass-cache
/connect.lock
/coverage
/libpeerconnection.log
npm-debug.log
testem.log
/typings
yarn-error.log
# e2e
/e2e/*.js
/e2e/*.map
protractor-results*.txt
# System Files
.DS_Store
Thumbs.db

View File

@ -1,92 +0,0 @@
# Angular documentation project (https://angular.io)
Everything in this folder is part of the documentation project. This includes
* the web site for displaying the documentation
* the dgeni configuration for converting source files to rendered files that can be viewed in the web site.
* the tooling for setting up examples for development; and generating plunkers and zip files from the examples.
## Developer tasks
We use `yarn` to manage the dependencies and to run build tasks.
You should run all these tasks from the `angular/aio` folder.
Here are the most important tasks you might need to use:
* `yarn` - install all the dependencies.
* `yarn setup` - Install all the dependencies, boilerplate, plunkers, zips and runs dgeni on the docs.
* `yarn start` - run a development web server that watches the files; then builds the doc-viewer and reloads the page, as necessary.
* `yarn lint` - check that the doc-viewer code follows our style rules.
* `yarn test` - watch all the source files, for the doc-viewer, and run all the unit tests when any change.
* `yarn e2e` - run all the e2e tests for the doc-viewer.
* `yarn docs` - generate all the docs from the source files.
* `yarn docs-watch` - watch the Angular source and the docs files and run a short-circuited doc-gen for the docs that changed.
* `yarn docs-lint` - check that the doc gen code follows our style rules.
* `yarn docs-test` - run the unit tests for the doc generation code.
* `yarn boilerplate:add` - generate all the boilerplate code for the examples, so that they can be run locally.
* `yarn boilerplate:remove` - remove all the boilerplate code that was added via `yarn boilerplate:add`.
* `yarn generate-plunkers` - generate the plunker files that are used by the `live-example` tags in the docs.
* `yarn generate-zips` - generate the zip files from the examples. Zip available via the `live-example` tags in the docs.
## Guide to authoring
There are two types of content in the documentatation:
* **API docs**: descriptions of the modules, classes, interfaces, decorators, etc that make up the Angular platform.
API docs are generated directly from the source code.
The source code is contained in TypeScript files, located in the `angular/packages` folder.
Each API item may have a preceding comment, which contains JSDoc style tags and content.
The content is written in markdown.
* **Other content**: guides, tutorials, and other marketing material.
All other content is written using markdown in text files, located in the `angular/aio/content` folder.
More specifically, there are sub-folders that contain particular types of content: guides, tutorial and marketing.
We use the [dgeni](https://github.com/angular/dgeni) tool to convert these files into docs that can be viewed in the doc-viewer.
### Generating the complete docs
The main task for generating the docs is `yarn docs`. This will process all the source files (API and other),
extracting the documentation and generating JSON files that can be consumed by the doc-viewer.
### Partial doc generation for editors
Full doc generation can take up to one minute. That's too slow for efficient document creation and editing.
You can make small changes in a smart editor that displays formatted markdown:
>In VS Code, _Cmd-K, V_ opens markdown preview in side pane; _Cmd-B_ toggles left sidebar
You also want to see those changes displayed properly in the doc viewer
with a quick, edit/view cycle time.
For this purpose, use the `yarn docs-watch` task, which watches for changes to source files and only
re-processes the the files necessary to generate the docs that are related to the file that has changed.
Since this task takes shortcuts, it is much faster (often less than 1 second) but it won't produce full
fidelity content. For example, links to other docs and code examples may not render correctly. This is
most particularly noticed in links to other docs and in the embedded examples, which may not always render
correctly.
The general setup is as follows:
* Open a terminal, ensure the dependencies are installed; run an initial doc generation; then start the doc-viewer:
```bash
yarn
yarn docs
yarn start
```
* Open a second terminal and start watching the docs
```bash
yarn docs-watch
```
* Open a browser at https://localhost:4200/ and navigate to the document on which you want to work.
You can automatically open the browser by using `yarn start -- -o` in the first terminal.
* Make changes to the page's associated doc or example files. Every time a file is saved, the doc will
be regenerated, the app will rebuild and the page will reload.

View File

@ -1,3 +0,0 @@
scripts-js/lib
scripts-js/node_modules
scripts-js/**/test

View File

@ -1,163 +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 log rotation
COPY logrotate/* /etc/logrotate.d/
RUN chmod 0644 /etc/logrotate.d/*
# Set up cronjobs
COPY cronjobs/aio-builds-cleanup /etc/cron.d/
RUN chmod 0744 /etc/cron.d/aio-builds-cleanup
RUN crontab /etc/cron.d/aio-builds-cleanup
RUN printenv | grep AIO_ >> /etc/environment
# Set up dnsmasq
COPY dnsmasq/dnsmasq.conf /etc/
RUN sed -i "s|{{\$AIO_NGINX_HOSTNAME}}|$AIO_NGINX_HOSTNAME|g" /etc/dnsmasq.conf
RUN sed -i "s|{{\$AIO_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

View File

@ -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

View File

@ -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

View File

@ -1,9 +0,0 @@
/var/log/aio/clean-up.log /var/log/aio/init.log /var/log/aio/verify-setup.log {
compress
create
delaycompress
missingok
monthly
notifempty
rotate 6
}

View File

@ -1,13 +0,0 @@
/var/log/aio/nginx/*.log /var/log/aio/nginx-test/*.log {
compress
create
delaycompress
missingok
monthly
notifempty
rotate 6
sharedscripts
postrotate
service nginx rotate >/dev/null 2>&1
endscript
}

View File

@ -1,9 +0,0 @@
/var/log/aio/upload-server-*.log {
compress
copytruncate
delaycompress
missingok
monthly
notifempty
rotate 6
}

View File

@ -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;
}
}

View File

@ -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

View File

@ -1,2 +0,0 @@
/dist
/node_modules

View File

@ -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));
}
}

View File

@ -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);
});
}

View File

@ -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('&');
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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();
};

View File

@ -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 || '';
};

View File

@ -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(409, `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);
}
});
});
}
}

View File

@ -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);
}
}

View File

@ -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(', ')}`,
));
}
}

View File

@ -1,39 +0,0 @@
// Imports
import {getEnvVar} from '../common/utils';
import {BuildVerifier} from './build-verifier';
// Run
_main();
// Functions
function _main() {
const secret = 'unused';
const githubToken = getEnvVar('AIO_GITHUB_TOKEN');
const repoSlug = getEnvVar('AIO_REPO_SLUG');
const organization = getEnvVar('AIO_GITHUB_ORGANIZATION');
const allowedTeamSlugs = getEnvVar('AIO_GITHUB_TEAM_SLUGS').split(',');
const 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);
}

View File

@ -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');

View File

@ -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);
}

View File

@ -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);
}
}

View File

@ -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} 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();

View File

@ -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();

View File

@ -1,6 +0,0 @@
// Imports
import {runTests} from '../common/run-tests';
// Run
const specFiles = [`${__dirname}/**/*.e2e.js`];
runTests(specFiles);

View File

@ -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);
});
});
}));
});

View File

@ -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(409)).
then(() => Promise.all([
getFile(pr9, sha9, 'index.html').then(h.verifyResponse(200, idxContentRegex9)),
getFile(pr9, sha9, 'foo/bar.js').then(h.verifyResponse(200, barContentRegex9)),
])).
then(done);
});
}));

View File

@ -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(409, /^Request to overwrite existing directory/)).
then(() => expect(h.readBuildFile(pr, sha9, 'index.html')).toBe('My content')).
then(done);
});
it('should delete the PR directory on error (for new PR)', done => {
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);
});
});
});

View File

@ -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"
}
}

View File

@ -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();
});
});
});

View File

@ -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');
});
});
});
});

View File

@ -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');
});
});
});

View File

@ -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();
});
});
});
});

View File

@ -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('');
});
});
});

View File

@ -1,6 +0,0 @@
declare namespace jasmine {
export interface DoneFn extends Function {
(): void;
fail: (message: Error | string) => void;
}
}

View File

@ -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);

View File

@ -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, 409, `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]();
});
});
});
});

View File

@ -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');
});
});

View File

@ -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);
});
});
});

View File

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

View File

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

View File

@ -1,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 1234567890 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);
});
});
});
});

View File

@ -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/**/*"
]
}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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.

View File

@ -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}

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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.

View File

@ -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

View File

@ -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`

View File

@ -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/resource`
- nginx maps the subdomain to the correct sub-directory 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.

View File

@ -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.

View File

@ -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`).

View File

@ -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
```

View File

@ -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).

View File

@ -1,75 +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). Log files are
rotated and retained for 6 months.
The following 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).

View File

@ -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`

View File

@ -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/*`

View File

@ -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
```

View File

@ -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

View File

@ -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"

View File

@ -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 -

View File

@ -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"

View File

@ -1,70 +0,0 @@
# boilerplate files
**/src/styles.css
**/src/systemjs-angular-loader.js
**/src/systemjs.config.js
**/src/tsconfig.json
**/bs-config.e2e.json
**/bs-config.json
**/package.json
**/tslint.json
**/karma.conf.js
**/karma-test-shim.js
**/browser-test-shim.js
**/node_modules
# built files
*.map
_test-output
protractor-helpers.js
*/e2e-spec.js
**/*.js
**/ts/**/*.js
**/js-es6*/**/*.js
dist/
# special
!/*
!*.1.*
!*.2.*
!*.3.*
*.1.js
*.2.js
*.3.js
*.1.js.map
*.2.js.map
*.3.js.map
!systemjs.config.*.js
!karma-test-shim.*.js
!copy-dist-files.js
# AngularJS files
!**/*.ajs.js
**/app/**/*.ajs.js
# aot
**/*.ngfactory.ts
**/*.ngsummary.json
**/*.shim.ngstyle.ts
**/*.metadata.json
!aot/bs-config.json
!aot/index.html
!rollup-config.js
# testing
!testing/src/browser-test-shim.js
!testing/karma*.js
# TS to JS
!ts-to-js/js*/**/*.js
ts-to-js/js*/**/system*.js
# webpack
!webpack/**/config/*.js
!webpack/**/*webpack*.js
# styleguide
!styleguide/src/systemjs.custom.js
# plunkers
*plnkr.no-link.html

View File

@ -1,115 +0,0 @@
'use strict'; // necessary for es6 output in node
import { browser, element, by } from 'protractor';
describe('AngularJS to Angular Quick Reference Tests', function () {
beforeAll(function () {
browser.get('');
});
it('should display no poster images after bootstrap', function () {
testImagesAreDisplayed(false);
});
it('should display proper movie data', function () {
// We check only a few samples
let expectedSamples: any[] = [
{row: 0, column: 0, element: 'img', attr: 'src', value: 'images/hero.png', contains: true},
{row: 0, column: 2, value: 'Celeritas'},
{row: 1, column: 3, matches: /Dec 1[678], 2015/}, // absorb timezone dif; we care about date format
{row: 1, column: 5, value: '$14.95'},
{row: 2, column: 4, value: 'PG-13'},
{row: 2, column: 7, value: '100%'},
{row: 2, column: 0, element: 'img', attr: 'src', value: 'images/ng-logo.png', contains: true},
];
// Go through the samples
let movieRows = getMovieRows();
for (let i = 0; i < expectedSamples.length; i++) {
let sample = expectedSamples[i];
let tableCell = movieRows.get(sample.row)
.all(by.tagName('td')).get(sample.column);
// Check the cell or its nested element
let elementToCheck = sample.element
? tableCell.element(by.tagName(sample.element))
: tableCell;
// Check element attribute or text
let valueToCheck = sample.attr
? elementToCheck.getAttribute(sample.attr)
: elementToCheck.getText();
// Test for equals/contains/match
if (sample.contains) {
expect(valueToCheck).toContain(sample.value);
} else if (sample.matches) {
expect(valueToCheck).toMatch(sample.matches);
} else {
expect(valueToCheck).toEqual(sample.value);
}
}
});
it('should display images after Show Poster', function () {
testPosterButtonClick('Show Poster', true);
});
it('should hide images after Hide Poster', function () {
testPosterButtonClick('Hide Poster', false);
});
it('should display no movie when no favorite hero is specified', function () {
testFavoriteHero(null, 'Please enter your favorite hero.');
});
it('should display no movie for Magneta', function () {
testFavoriteHero('Magneta', 'No movie, sorry!');
});
it('should display a movie for Mr. Nice', function () {
testFavoriteHero('Mr. Nice', 'Excellent choice!');
});
function testImagesAreDisplayed(isDisplayed: boolean) {
let expectedMovieCount = 3;
let movieRows = getMovieRows();
expect(movieRows.count()).toBe(expectedMovieCount);
for (let i = 0; i < expectedMovieCount; i++) {
let movieImage = movieRows.get(i).element(by.css('td > img'));
expect(movieImage.isDisplayed()).toBe(isDisplayed);
}
}
function testPosterButtonClick(expectedButtonText: string, isDisplayed: boolean) {
let posterButton = element(by.css('movie-list tr > th > button'));
expect(posterButton.getText()).toBe(expectedButtonText);
posterButton.click().then(function () {
testImagesAreDisplayed(isDisplayed);
});
}
function getMovieRows() {
return element.all(by.css('movie-list tbody > tr'));
}
function testFavoriteHero(heroName: string, expectedLabel: string) {
let movieListComp = element(by.tagName('movie-list'));
let heroInput = movieListComp.element(by.tagName('input'));
let favoriteHeroLabel = movieListComp.element(by.tagName('h3'));
let resultLabel = movieListComp.element(by.css('span > p'));
heroInput.clear().then(function () {
heroInput.sendKeys(heroName || '');
expect(resultLabel.getText()).toBe(expectedLabel);
if (heroName) {
expect(favoriteHeroLabel.isDisplayed()).toBe(true);
expect(favoriteHeroLabel.getText()).toContain(heroName);
} else {
expect(favoriteHeroLabel.isDisplayed()).toBe(false);
}
});
}
});

View File

@ -1,10 +0,0 @@
{
"description": "AngularJS to Angular Quick Reference",
"basePath": "src/",
"files":[
"!**/*.d.ts",
"!**/*.js",
"!**/*.[1].*"
],
"tags":["cookbook", "angularjs"]
}

View File

@ -1,16 +0,0 @@
// #docregion
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { MovieListComponent } from './movie-list.component';
const routes: Routes = [
{ path: '', redirectTo: '/movies', pathMatch: 'full' },
{ path: 'movies', component: MovieListComponent }
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule {}

View File

@ -1,9 +0,0 @@
.active {font-style: italic;}
.shazam {font-weight: bold;}
img {height: 100px;}
table td {
padding: 4px;
border: 1px solid #e0e0e0;
}

View File

@ -1,112 +0,0 @@
<!-- #docplaster -->
<h1>{{title}}</h1>
<h3>Routed Movies</h3>
<nav>
<!-- #docregion router-link -->
<a [routerLink]="['/movies']">Movies</a>
<!-- #enddocregion router-link -->
</nav>
<router-outlet></router-outlet>
<hr>
<h1>Example Snippets</h1>
<!-- #docregion ngClass -->
<div [ngClass]="{active: isActive}">
<!-- #enddocregion ngClass -->
[ngClass] active
</div>
<!-- #docregion ngClass -->
<div [ngClass]="{active: isActive,
shazam: isImportant}">
<!-- #enddocregion ngClass -->
[ngClass] active and boldly important
</div>
<!-- #docregion ngClass -->
<div [class.active]="isActive">
<!-- #enddocregion ngClass -->
[class.active]
</div>
<p></p>
<!-- #docregion href -->
<a [href]="angularDocsUrl">Angular Docs</a>
<!-- #enddocregion href -->
<p></p>
<div>
<!-- #docregion event-binding -->
<button (click)="toggleImage()">
<!-- #enddocregion event-binding -->
Image Toggle #1</button>
<!-- #docregion event-binding -->
<button (click)="toggleImage($event)">
<!-- #enddocregion event-binding -->
Image Toggle #2</button>
<p>Image toggle event type was {{eventType}}</p>
</div>
<p></p>
<div *ngIf="showImage">
<!-- #docregion src -->
<img [src]="movie.imageurl">
<!-- #enddocregion src -->
</div>
<p></p>
<!-- #docregion ngStyle -->
<div [ngStyle]="{color: colorPreference}">
<!-- #enddocregion ngStyle -->
color preference #1
</div>
<!-- #docregion ngStyle -->
<div [style.color]="colorPreference">
<!-- #enddocregion ngStyle -->
color preference #2
</div>
<h3>Movie as JSON</h3>
<!-- #docregion json -->
<pre>{{movie | json}}</pre>
<!-- #enddocregion json -->
<h3>Movie Titles via local variable</h3>
<table>
<!-- #docregion local -->
<tr *ngFor="let movie of movies">
<td>{{movie.title}}</td>
</tr>
<!-- #enddocregion local -->
</table>
<h3>Sliced Movies with pipes</h3>
<table>
<!-- #docregion slice -->
<tr *ngFor="let movie of movies | slice:0:2">
<!-- #enddocregion slice -->
<!-- #docregion uppercase -->
<td>{{movie.title | uppercase}}</td>
<!-- #enddocregion uppercase -->
<!-- #docregion lowercase -->
<td>{{movie.title | lowercase}}</td>
<!-- #enddocregion lowercase -->
<!-- #docregion date -->
<td>{{movie.releaseDate | date}}</td>
<!-- #enddocregion date -->
<!-- #docregion currency -->
<td>{{movie.price | currency:'USD':true}}</td>
<!-- #enddocregion currency -->
<!-- #docregion number -->
<td>{{movie.starRating | number}}</td>
<td>{{movie.starRating | number:'1.1-2'}}</td>
<td>{{movie.approvalRating | percent: '1.0-2'}}</td>
<!-- #enddocregion number -->
</tr></table>

View File

@ -1,32 +0,0 @@
import { Component } from '@angular/core';
import { MovieService } from './movie.service';
import { IMovie } from './movie';
@Component({
selector: 'my-app',
templateUrl: './app.component.html',
styleUrls: [ './app.component.css' ],
providers: [ MovieService ]
})
export class AppComponent {
angularDocsUrl = 'https://angular.io/';
colorPreference = 'red';
eventType = '<not clicked yet>';
isActive = true;
isImportant = true;
movie: IMovie = null;
movies: IMovie[] = [];
showImage = true;
title: string = 'AngularJS to Angular Quick Ref Cookbook';
toggleImage(event: UIEvent) {
this.showImage = !this.showImage;
this.eventType = (event && event.type) || 'not provided';
}
constructor(movieService: MovieService) {
this.movies = movieService.getMovies();
this.movie = this.movies[0];
}
}

View File

@ -1,12 +0,0 @@
// #docregion
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppComponent } from './app.component';
@NgModule({
imports: [ BrowserModule ],
declarations: [ AppComponent ],
bootstrap: [ AppComponent ]
})
export class AppModule { }

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