Compare commits
46 Commits
Author | SHA1 | Date | |
---|---|---|---|
7edc5e96f3 | |||
7b802faa96 | |||
6f039d7a3c | |||
0a82f7d69f | |||
b7f85813d5 | |||
a5bdbed306 | |||
883ca285de | |||
afb7540067 | |||
47df3d681f | |||
cd7fe31fbc | |||
4203b1bb1f | |||
055db5ad6a | |||
62a8618536 | |||
419fe0ca0d | |||
e46a65f8fd | |||
f0e1043a1d | |||
c1938f5af1 | |||
915eae50b4 | |||
68675e625a | |||
d0e1688514 | |||
ee6705af49 | |||
9218812011 | |||
ec77bf9fb0 | |||
fee5b2ad56 | |||
9c70a3cfb1 | |||
ec3b6e9603 | |||
63066f7697 | |||
5f6d0f2340 | |||
4a0e93b03b | |||
1f5dce2128 | |||
14e7e43ad8 | |||
54d4b893fd | |||
4670cf51cc | |||
dd4e501999 | |||
9124994849 | |||
4fbc61469f | |||
8a883f24f6 | |||
07cef367ac | |||
c060110695 | |||
93ff3166ab | |||
85a1b54c6e | |||
acf83b90bc | |||
f66e59ebe4 | |||
d932e724ab | |||
dcaa11a88b | |||
427d63a422 |
@ -1,19 +0,0 @@
|
||||
# Encryption
|
||||
|
||||
Based on https://github.com/circleci/encrypted-files
|
||||
|
||||
In the CircleCI web UI, we have a secret variable called `KEY`
|
||||
https://circleci.com/gh/angular/angular/edit#env-vars
|
||||
which is only exposed to non-fork builds
|
||||
(see "Pass secrets to builds from forked pull requests" under
|
||||
https://circleci.com/gh/angular/angular/edit#advanced-settings)
|
||||
|
||||
We use this as a symmetric AES encryption key to encrypt tokens like
|
||||
a GitHub token that enables publishing snapshots.
|
||||
|
||||
To create the github_token file, we take this approach:
|
||||
- Find the angular-builds:token in http://valentine
|
||||
- Go inside the ngcontainer docker image so you use the same version of openssl as we will at runtime: `docker run --rm -it angular/ngcontainer`
|
||||
- echo "https://[token]:@github.com" > credentials
|
||||
- openssl aes-256-cbc -e -in credentials -out .circleci/github_token -k $KEY
|
||||
- If needed, base64-encode the result so you can copy-paste it out of docker: `base64 github_token`
|
@ -1,39 +0,0 @@
|
||||
# These options are enabled when running on CI
|
||||
# We do this by copying this file to /etc/bazel.bazelrc at the start of the build.
|
||||
# See remote cache documentation in /docs/BAZEL.md
|
||||
|
||||
# Don't be spammy in the logs
|
||||
build --noshow_progress
|
||||
|
||||
# Don't run manual tests
|
||||
test --test_tag_filters=-manual
|
||||
|
||||
# Print all the options that apply to the build.
|
||||
# This helps us diagnose which options override others
|
||||
# (e.g. /etc/bazel.bazelrc vs. tools/bazel.rc)
|
||||
build --announce_rc
|
||||
|
||||
# Create dist/bin symlink to $(bazel info bazel-bin)
|
||||
# We use this when uploading artifacts after the build finishes
|
||||
build --symlink_prefix=dist/
|
||||
|
||||
# Enable experimental CircleCI bazel remote cache proxy
|
||||
# See remote cache documentation in /docs/BAZEL.md
|
||||
build --experimental_remote_spawn_cache --remote_rest_cache=http://localhost:7643
|
||||
|
||||
# Prevent unstable environment variables from tainting cache keys
|
||||
build --experimental_strict_action_env
|
||||
|
||||
# Save downloaded repositories such as the go toolchain
|
||||
# This directory can then be included in the CircleCI cache
|
||||
# It should save time running the first build
|
||||
build --experimental_repository_cache=/home/circleci/bazel_repository_cache
|
||||
|
||||
# Workaround https://github.com/bazelbuild/bazel/issues/3645
|
||||
# Bazel doesn't calculate the memory ceiling correctly when running under Docker.
|
||||
# Limit Bazel to consuming resources that fit in CircleCI "xlarge" class
|
||||
# https://circleci.com/docs/2.0/configuration-reference/#resource_class
|
||||
build --local_resources=14336,8.0,1.0
|
||||
|
||||
# Retry in the event of flakes, eg. https://circleci.com/gh/angular/angular/31309
|
||||
test --flaky_test_attempts=2
|
@ -1,229 +0,0 @@
|
||||
# Configuration file for https://circleci.com/gh/angular/angular
|
||||
|
||||
# Note: YAML anchors allow an object to be re-used, reducing duplication.
|
||||
# The ampersand declares an alias for an object, then later the `<<: *name`
|
||||
# syntax dereferences it.
|
||||
# See http://blog.daemonl.com/2016/02/yaml.html
|
||||
# To validate changes, use an online parser, eg.
|
||||
# http://yaml-online-parser.appspot.com/
|
||||
|
||||
# Variables
|
||||
|
||||
## IMPORTANT
|
||||
# If you change the `docker_image` version, also change the `cache_key` suffix and the version of
|
||||
# `com_github_bazelbuild_buildtools` in the `/WORKSPACE` file.
|
||||
var_1: &docker_image angular/ngcontainer:0.3.1
|
||||
var_2: &cache_key v2-angular-{{ .Branch }}-{{ checksum "yarn.lock" }}-0.3.1
|
||||
|
||||
# Define common ENV vars
|
||||
var_3: &define_env_vars
|
||||
run: echo "export PROJECT_ROOT=$(pwd)" >> $BASH_ENV
|
||||
|
||||
# See remote cache documentation in /docs/BAZEL.md
|
||||
var_4: &setup-bazel-remote-cache
|
||||
run:
|
||||
name: Start up bazel remote cache proxy
|
||||
command: ~/bazel-remote-proxy -backend circleci://
|
||||
background: true
|
||||
|
||||
# Settings common to each job
|
||||
anchor_1: &job_defaults
|
||||
working_directory: ~/ng
|
||||
docker:
|
||||
- image: *docker_image
|
||||
|
||||
# After checkout, rebase on top of master.
|
||||
# Similar to travis behavior, but not quite the same.
|
||||
# See https://discuss.circleci.com/t/1662
|
||||
anchor_2: &post_checkout
|
||||
post: git pull --ff-only origin "refs/pull/${CIRCLE_PULL_REQUEST//*pull\//}/merge"
|
||||
|
||||
version: 2
|
||||
jobs:
|
||||
lint:
|
||||
<<: *job_defaults
|
||||
steps:
|
||||
- checkout:
|
||||
<<: *post_checkout
|
||||
|
||||
# Check BUILD.bazel formatting before we have a node_modules directory
|
||||
# Then we don't need any exclude pattern to avoid checking those files
|
||||
- run: 'buildifier -mode=check $(find . -type f \( -name BUILD.bazel -or -name BUILD \)) ||
|
||||
(echo "BUILD files not formatted. Please run ''yarn buildifier''" ; exit 1)'
|
||||
# Run the skylark linter to check our Bazel rules
|
||||
# deprecated-api is disabled because we use actions.new_file(genfiles_dir)
|
||||
# which has no replacement, see https://github.com/bazelbuild/bazel/issues/4858
|
||||
- run: 'find . -type f -name "*.bzl" |
|
||||
xargs java -jar /usr/local/bin/Skylint_deploy.jar --disable-checks=deprecated-api ||
|
||||
(echo -e "\n.bzl files have lint errors. Please run ''yarn skylint''"; exit 1)'
|
||||
|
||||
- restore_cache:
|
||||
key: *cache_key
|
||||
|
||||
- run: yarn install --frozen-lockfile --non-interactive
|
||||
- run: ./node_modules/.bin/gulp lint
|
||||
|
||||
test:
|
||||
<<: *job_defaults
|
||||
resource_class: xlarge
|
||||
steps:
|
||||
- *define_env_vars
|
||||
- checkout:
|
||||
<<: *post_checkout
|
||||
# See remote cache documentation in /docs/BAZEL.md
|
||||
- run: .circleci/setup_cache.sh
|
||||
- run: sudo cp .circleci/bazel.rc /etc/bazel.bazelrc
|
||||
- *setup-bazel-remote-cache
|
||||
|
||||
- restore_cache:
|
||||
key: *cache_key
|
||||
|
||||
- run: ls /home/circleci/bazel_repository_cache || true
|
||||
- run: bazel info release
|
||||
- run: bazel run @nodejs//:yarn
|
||||
# Use bazel query so that we explicitly ask for all buildable targets to be built as well
|
||||
# This avoids waiting for the slowest build target to finish before running the first test
|
||||
# See https://github.com/bazelbuild/bazel/issues/4257
|
||||
# NOTE: Angular developers should typically just bazel build //packages/... or bazel test //packages/...
|
||||
- run: bazel query --output=label //... | xargs bazel test
|
||||
|
||||
# CircleCI will allow us to go back and view/download these artifacts from past builds.
|
||||
# Also we can use a service like https://buildsize.org/ to automatically track binary size of these artifacts.
|
||||
# The destination keys need be format {projectName}/{context}/{fileName} so that the github-robot can process them for size calculations
|
||||
# projectName should remain consistant to group files
|
||||
# context and fileName can be almost anything (within usual URI rules)
|
||||
# There should only be exactly 2 forward slashes in the path
|
||||
# This is so they're backwards compatiable with the existing data we have on bundle sizes
|
||||
- store_artifacts:
|
||||
path: dist/bin/packages/core/test/bundling/hello_world/bundle.min.js
|
||||
destination: core/hello_world/bundle
|
||||
- store_artifacts:
|
||||
path: dist/bin/packages/core/test/bundling/todo/bundle.min.js
|
||||
destination: core/todo/bundle
|
||||
- store_artifacts:
|
||||
path: dist/bin/packages/core/test/bundling/hello_world/bundle.min.js.br
|
||||
destination: core/hello_world/bundle.br
|
||||
- store_artifacts:
|
||||
path: dist/bin/packages/core/test/bundling/todo/bundle.min.js.br
|
||||
destination: core/todo/bundle.br
|
||||
- save_cache:
|
||||
key: *cache_key
|
||||
paths:
|
||||
- "node_modules"
|
||||
- "~/bazel_repository_cache"
|
||||
|
||||
# This job exists only for backwards-compatibility with old scripts and tests
|
||||
# that rely on the pre-Bazel dist/packages-dist layout.
|
||||
# It duplicates some work with the job above: we build the bazel packages
|
||||
# twice. Even though we have a remote cache, these jobs will typically run in
|
||||
# parallel so up-to-date outputs will not be available at the time the build
|
||||
# starts.
|
||||
# No new jobs should depend on this one.
|
||||
build-packages-dist:
|
||||
<<: *job_defaults
|
||||
resource_class: xlarge
|
||||
steps:
|
||||
- *define_env_vars
|
||||
- checkout:
|
||||
<<: *post_checkout
|
||||
# See remote cache documentation in /docs/BAZEL.md
|
||||
- run: .circleci/setup_cache.sh
|
||||
- run: sudo cp .circleci/bazel.rc /etc/bazel.bazelrc
|
||||
- *setup-bazel-remote-cache
|
||||
|
||||
- run: bazel run @nodejs//:yarn
|
||||
- run: scripts/build-packages-dist.sh
|
||||
|
||||
# Save the npm packages from //packages/... for other workflow jobs to read
|
||||
# https://circleci.com/docs/2.0/workflows/#using-workspaces-to-share-data-among-jobs
|
||||
- persist_to_workspace:
|
||||
root: dist
|
||||
paths:
|
||||
- packages-dist
|
||||
|
||||
# We run the integration tests outside of Bazel for now.
|
||||
# They are a separate workflow job so that they can be easily re-run.
|
||||
# When the tests are ported to bazel test targets, they should move to the "test"
|
||||
# job above, as part of the bazel test command. That has flaky_test_attempts so the
|
||||
# need to re-run manually should be alleviated.
|
||||
# See comments inside the integration/run_tests.sh script.
|
||||
integration_test:
|
||||
<<: *job_defaults
|
||||
steps:
|
||||
- *define_env_vars
|
||||
- checkout:
|
||||
<<: *post_checkout
|
||||
- attach_workspace:
|
||||
at: dist
|
||||
- run: xvfb-run --auto-servernum ./integration/run_tests.sh
|
||||
|
||||
# This job updates the content of repos like github.com/angular/core-builds
|
||||
# for every green build on angular/angular.
|
||||
publish_snapshot:
|
||||
<<: *job_defaults
|
||||
steps:
|
||||
# See below - ideally this job should not trigger for non-upstream builds.
|
||||
# But since it does, we have to check this condition.
|
||||
- run:
|
||||
name: Skip this job for Pull Requests and Fork builds
|
||||
# Note, `|| true` on the end makes this step always exit 0
|
||||
command: '[[
|
||||
-v CIRCLE_PR_NUMBER
|
||||
|| "$CIRCLE_PROJECT_USERNAME" != "angular"
|
||||
|| "$CIRCLE_PROJECT_REPONAME" != "angular"
|
||||
]] && circleci step halt || true'
|
||||
- checkout:
|
||||
<<: *post_checkout
|
||||
- attach_workspace:
|
||||
at: dist
|
||||
# CircleCI has a config setting to force SSH for all github connections
|
||||
# This is not compatible with our mechanism of using a Personal Access Token
|
||||
# Clear the global setting
|
||||
- run: git config --global --unset "url.ssh://git@github.com.insteadof"
|
||||
- run:
|
||||
name: Decrypt github credentials
|
||||
command: 'openssl aes-256-cbc -d -in .circleci/github_token -k "${KEY}" -out ~/.git_credentials'
|
||||
- run: ./scripts/ci/publish-build-artifacts.sh
|
||||
|
||||
aio_monitoring:
|
||||
<<: *job_defaults
|
||||
steps:
|
||||
- checkout:
|
||||
<<: *post_checkout
|
||||
- restore_cache:
|
||||
key: *cache_key
|
||||
- run: xvfb-run --auto-servernum ./aio/scripts/test-production.sh
|
||||
|
||||
workflows:
|
||||
version: 2
|
||||
default_workflow:
|
||||
jobs:
|
||||
- lint
|
||||
- test
|
||||
- build-packages-dist
|
||||
- integration_test:
|
||||
requires:
|
||||
- build-packages-dist
|
||||
- publish_snapshot:
|
||||
# Note: no filters on this job because we want it to run for all upstream branches
|
||||
# We'd really like to filter out pull requests here, but not yet available:
|
||||
# https://discuss.circleci.com/t/workflows-pull-request-filter/14396/4
|
||||
# Instead, the job just exits immediately at the first step.
|
||||
requires:
|
||||
# Only publish if tests and integration tests pass
|
||||
- test
|
||||
- integration_test
|
||||
# Get the artifacts to publish from the build-packages-dist job
|
||||
# since the publishing script expects the legacy outputs layout.
|
||||
- build-packages-dist
|
||||
|
||||
aio_monitoring:
|
||||
jobs:
|
||||
- aio_monitoring
|
||||
triggers:
|
||||
- schedule:
|
||||
cron: "0 0 * * *"
|
||||
filters:
|
||||
branches:
|
||||
only:
|
||||
- master
|
Binary file not shown.
@ -1,11 +0,0 @@
|
||||
#!/bin/sh
|
||||
# Install bazel remote cache proxy
|
||||
# This is temporary until the feature is no longer experimental on CircleCI.
|
||||
# See remote cache documentation in /docs/BAZEL.md
|
||||
|
||||
set -u -e
|
||||
|
||||
readonly DOWNLOAD_URL="https://5-116431813-gh.circle-artifacts.com/0/pkg/bazel-remote-proxy-$(uname -s)_$(uname -m)"
|
||||
|
||||
curl --fail -o ~/bazel-remote-proxy "$DOWNLOAD_URL"
|
||||
chmod +x ~/bazel-remote-proxy
|
68
.github/ISSUE_TEMPLATE.md
vendored
68
.github/ISSUE_TEMPLATE.md
vendored
@ -1,59 +1,39 @@
|
||||
<!--
|
||||
PLEASE HELP US PROCESS GITHUB ISSUES FASTER BY PROVIDING THE FOLLOWING INFORMATION.
|
||||
|
||||
ISSUES MISSING IMPORTANT INFORMATION MAY BE CLOSED WITHOUT INVESTIGATION.
|
||||
IF YOU DON'T FILL OUT THE FOLLOWING INFORMATION WE MIGHT CLOSE YOUR ISSUE WITHOUT INVESTIGATING
|
||||
-->
|
||||
|
||||
## I'm submitting a...
|
||||
<!-- Check one of the following options with "x" -->
|
||||
<pre><code>
|
||||
[ ] Regression (a behavior that used to work and stopped working in a new release)
|
||||
[ ] Bug report <!-- Please search GitHub for a similar issue or PR before submitting -->
|
||||
[ ] Performance issue
|
||||
[ ] Feature request
|
||||
[ ] Documentation issue or request
|
||||
[ ] Support request => Please do not submit support request here, instead see https://github.com/angular/angular/blob/master/CONTRIBUTING.md#question
|
||||
[ ] Other... Please describe:
|
||||
</code></pre>
|
||||
**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 issue manifests. -->
|
||||
**Current behavior**
|
||||
<!-- Describe how the bug manifests. -->
|
||||
|
||||
**Expected behavior**
|
||||
<!-- Describe what the behavior would be without the bug. -->
|
||||
|
||||
## Expected behavior
|
||||
<!-- Describe what the desired behavior would be. -->
|
||||
|
||||
|
||||
## Minimal reproduction of the problem with instructions
|
||||
**Minimal reproduction of the problem with instructions**
|
||||
<!--
|
||||
For bug reports please provide the *STEPS TO REPRODUCE* and if possible a *MINIMAL DEMO* of the problem via
|
||||
https://stackblitz.com or similar (you can use this template as a starting point: https://stackblitz.com/fork/angular-gitter).
|
||||
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. -->
|
||||
**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, ... -->
|
||||
|
||||
## Environment
|
||||
|
||||
<pre><code>
|
||||
Angular version: X.Y.Z
|
||||
* **Angular version:** 2.0.X
|
||||
<!-- Check whether this is still an issue in the most recent Angular version -->
|
||||
|
||||
Browser:
|
||||
- [ ] Chrome (desktop) version XX
|
||||
- [ ] Chrome (Android) version XX
|
||||
- [ ] Chrome (iOS) version XX
|
||||
- [ ] Firefox version XX
|
||||
- [ ] Safari (desktop) version XX
|
||||
- [ ] Safari (iOS) version XX
|
||||
- [ ] IE version XX
|
||||
- [ ] Edge version XX
|
||||
* **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 -->
|
||||
|
||||
For Tooling issues:
|
||||
- Node version: XX <!-- run `node --version` -->
|
||||
- Platform: <!-- Mac, Linux, Windows -->
|
||||
* **Language:** [all | TypeScript X.X | ES6/7 | ES5]
|
||||
|
||||
Others:
|
||||
<!-- Anything else relevant? Operating system version, IDE, package manager, HTTP server, ... -->
|
||||
</code></pre>
|
||||
* **Node (for AoT issues):** `node --version` =
|
||||
|
27
.github/PULL_REQUEST_TEMPLATE.md
vendored
27
.github/PULL_REQUEST_TEMPLATE.md
vendored
@ -1,15 +1,10 @@
|
||||
## PR Checklist
|
||||
Please check if your PR fulfills the following requirements:
|
||||
|
||||
**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)
|
||||
|
||||
|
||||
## PR Type
|
||||
What kind of change does this PR introduce?
|
||||
|
||||
<!-- Please check the one that applies to this PR using "x". -->
|
||||
**What kind of change does this PR introduce?** (check one with "x")
|
||||
```
|
||||
[ ] Bugfix
|
||||
[ ] Feature
|
||||
@ -17,27 +12,25 @@ What kind of change does this PR introduce?
|
||||
[ ] Refactoring (no functional changes, no api changes)
|
||||
[ ] Build related changes
|
||||
[ ] CI related changes
|
||||
[ ] Documentation content changes
|
||||
[ ] angular.io application / infrastructure changes
|
||||
[ ] Other... Please describe:
|
||||
```
|
||||
|
||||
## What is the current behavior?
|
||||
<!-- Please describe the current behavior that you are modifying, or link to a relevant issue. -->
|
||||
|
||||
Issue Number: N/A
|
||||
**What is the current behavior?** (You can also link to an open issue here)
|
||||
|
||||
|
||||
## What is the new behavior?
|
||||
|
||||
**What is the new behavior?**
|
||||
|
||||
|
||||
## Does this PR introduce a breaking change?
|
||||
|
||||
**Does this PR introduce a breaking change?** (check one with "x")
|
||||
```
|
||||
[ ] Yes
|
||||
[ ] No
|
||||
```
|
||||
|
||||
<!-- If this PR contains a breaking change, please describe the impact and migration path for existing applications below. -->
|
||||
If this PR contains a breaking change, please describe the impact and migration path for existing applications: ...
|
||||
|
||||
|
||||
## Other information
|
||||
**Other information**:
|
||||
|
||||
|
122
.github/angular-robot.yml
vendored
122
.github/angular-robot.yml
vendored
@ -1,122 +0,0 @@
|
||||
# Configuration for angular-robot
|
||||
|
||||
#options for the size plugin
|
||||
size:
|
||||
disabled: false
|
||||
maxSizeIncrease: 1000
|
||||
circleCiStatusName: "ci/circleci: build-packages-dist"
|
||||
status:
|
||||
disabled: false
|
||||
context: "ci/angular: size"
|
||||
|
||||
# options for the merge plugin
|
||||
merge:
|
||||
# the status will be added to your pull requests
|
||||
status:
|
||||
# set to true to disable
|
||||
disabled: false
|
||||
# the name of the status
|
||||
context: "ci/angular: merge status"
|
||||
# text to show when all checks pass
|
||||
successText: "All checks passed!"
|
||||
# text to show when some checks are failing
|
||||
failureText: "The following checks are failing:"
|
||||
|
||||
# the g3 status will be added to your pull requests if they include files that match the patterns
|
||||
g3Status:
|
||||
# set to true to disable
|
||||
disabled: false
|
||||
# the name of the status
|
||||
context: "google3"
|
||||
# text to show when the status is pending, {{PRNumber}} will be replaced by the PR number
|
||||
pendingDesc: "Googler: run g3sync presubmit {{PRNumber}}"
|
||||
# text to show when the status is success
|
||||
successDesc: "Does not affect google3"
|
||||
# link to use for the details
|
||||
url: "http://go/angular-g3sync"
|
||||
# list of patterns to check for the files changed by the PR
|
||||
# this list must be manually kept in sync with google3/third_party/javascript/angular2/copy.bara.sky
|
||||
include:
|
||||
- "LICENSE"
|
||||
- "modules/**"
|
||||
- "packages/**"
|
||||
# list of patterns to ignore for the files changed by the PR
|
||||
exclude:
|
||||
- "packages/language-service/**"
|
||||
- "**/.gitignore"
|
||||
- "**/.gitkeep"
|
||||
- "**/package.json"
|
||||
- "**/tsconfig-build.json"
|
||||
- "**/tsconfig.json"
|
||||
- "**/rollup.config.js"
|
||||
- "**/BUILD.bazel"
|
||||
- "packages/**/integrationtest/**"
|
||||
- "packages/**/test/**"
|
||||
|
||||
# comment that will be added to a PR when there is a conflict, leave empty or set to false to disable
|
||||
mergeConflictComment: "Hi @{{PRAuthor}}! This PR has merge conflicts due to recent upstream merges.
|
||||
\nPlease help to unblock it by resolving these conflicts. Thanks!"
|
||||
|
||||
# label to monitor
|
||||
mergeLabel: "PR action: merge"
|
||||
|
||||
# list of checks that will determine if the merge label can be added
|
||||
checks:
|
||||
# whether the PR shouldn't have a conflict with the base branch
|
||||
noConflict: true
|
||||
# list of labels that a PR needs to have, checked with a regexp (e.g. "PR target:" will work for the label "PR target: master")
|
||||
requiredLabels:
|
||||
- "PR target: *"
|
||||
- "cla: yes"
|
||||
|
||||
# list of labels that a PR shouldn't have, checked after the required labels with a regexp
|
||||
forbiddenLabels:
|
||||
- "PR target: TBD"
|
||||
- "PR action: cleanup"
|
||||
- "PR action: review"
|
||||
- "PR state: blocked"
|
||||
- "cla: no"
|
||||
|
||||
# list of PR statuses that need to be successful
|
||||
requiredStatuses:
|
||||
- "continuous-integration/travis-ci/pr"
|
||||
- "code-review/pullapprove"
|
||||
- "ci/circleci: build"
|
||||
- "ci/circleci: lint"
|
||||
|
||||
# the comment that will be added when the merge label is added despite failing checks, leave empty or set to false to disable
|
||||
# {{MERGE_LABEL}} will be replaced by the value of the mergeLabel option
|
||||
# {{PLACEHOLDER}} will be replaced by the list of failing checks
|
||||
mergeRemovedComment: "I see that you just added the `{{MERGE_LABEL}}` label, but the following checks are still failing:
|
||||
\n{{PLACEHOLDER}}
|
||||
\n
|
||||
\n**If you want your PR to be merged, it has to pass all the CI checks.**
|
||||
\n
|
||||
\nIf you can't get the PR to a green state due to flakes or broken master, please try rebasing to master and/or restarting the CI job. If that fails and you believe that the issue is not due to your change, please contact the caretaker and ask for help."
|
||||
|
||||
# options for the triage plugin
|
||||
triage:
|
||||
# number of the milestone to apply when the issue has not been triaged yet
|
||||
needsTriageMilestone: 83,
|
||||
# number of the milestone to apply when the issue is triaged
|
||||
defaultMilestone: 82,
|
||||
# arrays of labels that determine if an issue has been triaged by the caretaker
|
||||
l1TriageLabels:
|
||||
-
|
||||
- "comp: *"
|
||||
# arrays of labels that determine if an issue has been fully triaged
|
||||
l2TriageLabels:
|
||||
-
|
||||
- "type: bug/fix"
|
||||
- "severity*"
|
||||
- "freq*"
|
||||
- "comp: *"
|
||||
-
|
||||
- "type: feature"
|
||||
- "comp: *"
|
||||
-
|
||||
- "type: refactor"
|
||||
- "comp: *"
|
||||
-
|
||||
- "type: RFC / Discussion / question"
|
||||
- "comp: *"
|
4
.gitignore
vendored
4
.gitignore
vendored
@ -1,11 +1,8 @@
|
||||
.DS_STORE
|
||||
|
||||
/dist/
|
||||
bazel-*
|
||||
e2e_test.*
|
||||
node_modules
|
||||
bower_components
|
||||
tools/gulp-tasks/cldr/cldr-data/
|
||||
|
||||
# Include when developing application packages.
|
||||
pubspec.lock
|
||||
@ -14,7 +11,6 @@ pubspec.lock
|
||||
.settings/
|
||||
*.swo
|
||||
modules/.settings
|
||||
.bazelrc
|
||||
.vscode
|
||||
modules/.vscode
|
||||
|
||||
|
189
.pullapprove.yml
189
.pullapprove.yml
@ -7,26 +7,21 @@
|
||||
#
|
||||
# alexeagle - Alex Eagle
|
||||
# alxhub - Alex Rickabaugh
|
||||
# andrewseguin - Andrew Seguin
|
||||
# brandonroberts - Brandon Roberts
|
||||
# brocco - Mike Brocchi
|
||||
# filipesilva - Filipe Silva
|
||||
# chuckjaz - Chuck Jazdzewski
|
||||
# gkalpak - George Kalpakas
|
||||
# hansl - Hans Larsen
|
||||
# IgorMinar - Igor Minar
|
||||
# jasonaden - Jason Aden
|
||||
# kapunahelewong - Kapunahele Wong
|
||||
# kara - Kara Erickson
|
||||
# kyliau - Keen Yee Liau
|
||||
# matsko - Matias Niemelä
|
||||
# mhevery - Misko Hevery
|
||||
# petebacondarwin - Pete Bacon Darwin
|
||||
# pkozlowski-opensource - Pawel Kozlowski
|
||||
# robwormald - Rob Wormald
|
||||
# tinayuangao - Tina Gao
|
||||
# tbosch - Tobias Bosch
|
||||
# vicb - Victor Berchet
|
||||
# vikerman - Vikram Subramanian
|
||||
|
||||
# wardbell - Ward Bell
|
||||
# tinayuangao - Tina Gao
|
||||
|
||||
version: 2
|
||||
|
||||
@ -36,41 +31,20 @@ group_defaults:
|
||||
enabled: true
|
||||
approve_by_comment:
|
||||
enabled: false
|
||||
# see http://docs.pullapprove.com/groups/author_approval/
|
||||
author_approval:
|
||||
# If the author is a reviewer on the PR, they will automatically have an "approved" status.
|
||||
auto: true
|
||||
|
||||
groups:
|
||||
# Require all PRs to have at least one approval from *someone*
|
||||
all:
|
||||
users: all
|
||||
required: 1
|
||||
rejection_value: -999
|
||||
# In this group, your self-approval does not count
|
||||
author_approval:
|
||||
auto: false
|
||||
ignored: true
|
||||
files:
|
||||
include:
|
||||
- "*"
|
||||
|
||||
root:
|
||||
conditions:
|
||||
files:
|
||||
include:
|
||||
- "*"
|
||||
exclude:
|
||||
- "WORKSPACE"
|
||||
- "BUILD.bazel"
|
||||
- ".circleci/*"
|
||||
- "aio/*"
|
||||
- "integration/*"
|
||||
- "modules/*"
|
||||
- "packages/*"
|
||||
- "tools/*"
|
||||
users:
|
||||
- alexeagle
|
||||
- IgorMinar
|
||||
- mhevery
|
||||
|
||||
@ -83,22 +57,6 @@ groups:
|
||||
- IgorMinar
|
||||
- mhevery
|
||||
|
||||
bazel:
|
||||
conditions:
|
||||
files:
|
||||
include:
|
||||
- "WORKSPACE"
|
||||
- "*.bazel"
|
||||
- "*.bzl"
|
||||
- "packages/bazel/*"
|
||||
- "tools/bazel.rc"
|
||||
users:
|
||||
- alexeagle #primary
|
||||
- kyliau
|
||||
- IgorMinar #fallback
|
||||
- mhevery
|
||||
- vikerman #fallback
|
||||
|
||||
build-and-ci:
|
||||
conditions:
|
||||
files:
|
||||
@ -108,7 +66,7 @@ groups:
|
||||
- "*.lock"
|
||||
- "tools/*"
|
||||
exclude:
|
||||
- "tools/bazel.rc"
|
||||
- "tools/@angular/tsc-wrapped/*"
|
||||
- "tools/public_api_guard/*"
|
||||
- "aio/*"
|
||||
users:
|
||||
@ -124,29 +82,30 @@ groups:
|
||||
users:
|
||||
- alexeagle
|
||||
- mhevery
|
||||
- tbosch
|
||||
- vicb
|
||||
- IgorMinar #fallback
|
||||
|
||||
|
||||
core:
|
||||
conditions:
|
||||
files:
|
||||
- "packages/core/*"
|
||||
users:
|
||||
- mhevery #primary
|
||||
- jasonaden
|
||||
- kara
|
||||
- tbosch #primary
|
||||
- mhevery
|
||||
- vicb
|
||||
- IgorMinar #fallback
|
||||
|
||||
animations:
|
||||
compiler/animations:
|
||||
conditions:
|
||||
files:
|
||||
- "packages/animations/*"
|
||||
- "packages/platform-browser/animations/*"
|
||||
- "packages/compiler/src/animation/*"
|
||||
users:
|
||||
- matsko #primary
|
||||
- mhevery #fallback
|
||||
- tbosch
|
||||
- IgorMinar #fallback
|
||||
- mhevery #fallback
|
||||
|
||||
compiler/i18n:
|
||||
conditions:
|
||||
@ -154,7 +113,7 @@ groups:
|
||||
- "packages/compiler/src/i18n/*"
|
||||
users:
|
||||
- vicb #primary
|
||||
- alxhub
|
||||
- tbosch
|
||||
- IgorMinar #fallback
|
||||
- mhevery #fallback
|
||||
|
||||
@ -163,43 +122,28 @@ groups:
|
||||
files:
|
||||
- "packages/compiler/*"
|
||||
users:
|
||||
- chuckjaz #primary
|
||||
- tbosch #primary
|
||||
- vicb
|
||||
- chuckjaz
|
||||
- mhevery
|
||||
- IgorMinar #fallback
|
||||
|
||||
compiler-cli/ngtools:
|
||||
conditions:
|
||||
files:
|
||||
- "packages/compiler-cli/src/ngtools*"
|
||||
users:
|
||||
- hansl
|
||||
- filipesilva #fallback
|
||||
- brocco #fallback
|
||||
- IgorMinar #fallback
|
||||
|
||||
compiler-cli:
|
||||
conditions:
|
||||
files:
|
||||
include:
|
||||
- "packages/compiler-cli/*"
|
||||
- "packages/bazel/*"
|
||||
exclude:
|
||||
- "packages/compiler-cli/src/ngtools*"
|
||||
- "tools/@angular/tsc-wrapped/*"
|
||||
- "packages/compiler-cli/*"
|
||||
users:
|
||||
- alexeagle
|
||||
- alxhub
|
||||
- vicb
|
||||
- chuckjaz
|
||||
- tbosch
|
||||
- IgorMinar #fallback
|
||||
- mhevery #fallback
|
||||
|
||||
common:
|
||||
conditions:
|
||||
files:
|
||||
include:
|
||||
- "packages/common/*"
|
||||
exclude:
|
||||
- "packages/common/http/*"
|
||||
- "packages/common/*"
|
||||
users:
|
||||
- pkozlowski-opensource #primary
|
||||
- vicb
|
||||
@ -210,12 +154,6 @@ groups:
|
||||
conditions:
|
||||
files:
|
||||
- "packages/forms/*"
|
||||
- "aio/content/guide/forms.md"
|
||||
- "aio/content/guide/form-validation.md"
|
||||
- "aio/content/guide/reactive-forms.md"
|
||||
- "aio/content/examples/forms/*"
|
||||
- "aio/content/examples/form-validation/*"
|
||||
- "aio/content/examples/reactive-forms/*"
|
||||
users:
|
||||
- kara #primary
|
||||
- tinayuangao #secondary
|
||||
@ -225,11 +163,11 @@ groups:
|
||||
http:
|
||||
conditions:
|
||||
files:
|
||||
- "packages/common/http/*"
|
||||
- "packages/http/*"
|
||||
users:
|
||||
- alxhub #primary
|
||||
- IgorMinar
|
||||
- vikerman #primary
|
||||
- alxhub
|
||||
- IgorMinar #fallback
|
||||
- mhevery #fallback
|
||||
|
||||
language-service:
|
||||
@ -237,8 +175,8 @@ groups:
|
||||
files:
|
||||
- "packages/language-service/*"
|
||||
users:
|
||||
- kyliau #primary
|
||||
# needs secondary
|
||||
- chuckjaz #primary
|
||||
- tbosch #secondary
|
||||
- vicb
|
||||
- IgorMinar #fallback
|
||||
- mhevery #fallback
|
||||
@ -248,7 +186,7 @@ groups:
|
||||
files:
|
||||
- "packages/router/*"
|
||||
users:
|
||||
- jasonaden #primary
|
||||
- jasonaden
|
||||
- vicb
|
||||
- IgorMinar #fallback
|
||||
- mhevery #fallback
|
||||
@ -268,8 +206,8 @@ groups:
|
||||
files:
|
||||
- "packages/platform-browser/*"
|
||||
users:
|
||||
- vicb #primary
|
||||
# needs secondary
|
||||
- tbosch #primary
|
||||
- vicb #secondary
|
||||
- IgorMinar #fallback
|
||||
- mhevery #fallback
|
||||
|
||||
@ -279,8 +217,9 @@ groups:
|
||||
- "packages/platform-server/*"
|
||||
users:
|
||||
- vikerman #primary
|
||||
- alxhub #secondary
|
||||
- alxhub
|
||||
- vicb
|
||||
- tbosch
|
||||
- IgorMinar #fallback
|
||||
- mhevery #fallback
|
||||
|
||||
@ -290,37 +229,18 @@ groups:
|
||||
- "packages/platform-webworker/*"
|
||||
users:
|
||||
- vicb #primary
|
||||
# needs secondary
|
||||
- tbosch #secondary
|
||||
- IgorMinar #fallback
|
||||
- mhevery #fallback
|
||||
|
||||
service-worker:
|
||||
conditions:
|
||||
files:
|
||||
- "packages/service-worker/*"
|
||||
users:
|
||||
- alxhub #primary
|
||||
- gkalpak
|
||||
- IgorMinar #fallback
|
||||
- mhevery #fallback
|
||||
|
||||
elements:
|
||||
conditions:
|
||||
files:
|
||||
- "packages/elements/*"
|
||||
users:
|
||||
- andrewseguin #primary
|
||||
- gkalpak
|
||||
- robwormald
|
||||
- IgorMinar #fallback
|
||||
- mhevery #fallback
|
||||
|
||||
benchpress:
|
||||
conditions:
|
||||
files:
|
||||
- "packages/benchpress/*"
|
||||
users:
|
||||
- alxhub # primary
|
||||
- tbosch #primary
|
||||
# needs secondary
|
||||
- IgorMinar #fallback
|
||||
- mhevery #fallback
|
||||
@ -328,45 +248,10 @@ groups:
|
||||
angular.io:
|
||||
conditions:
|
||||
files:
|
||||
include:
|
||||
- "aio/*"
|
||||
exclude:
|
||||
- "aio/content/*"
|
||||
- "aio/*"
|
||||
users:
|
||||
- petebacondarwin #primary
|
||||
- IgorMinar
|
||||
- IgorMinar #primary
|
||||
- petebacondarwin #secondary
|
||||
- gkalpak
|
||||
- mhevery #fallback
|
||||
|
||||
angular.io-guide-and-tutorial:
|
||||
conditions:
|
||||
files:
|
||||
include:
|
||||
- "aio/content/*"
|
||||
exclude:
|
||||
- "aio/content/marketing/*"
|
||||
- "aio/content/navigation.json"
|
||||
- "aio/content/license.md"
|
||||
users:
|
||||
- kapunahelewong
|
||||
- stephenfluin
|
||||
- petebacondarwin
|
||||
- gkalpak
|
||||
- IgorMinar
|
||||
- brandonroberts
|
||||
- mhevery #fallback
|
||||
|
||||
angular.io-marketing:
|
||||
conditions:
|
||||
files:
|
||||
include:
|
||||
- "aio/content/marketing/*"
|
||||
- "aio/content/navigation.json"
|
||||
- "aio/content/license.md"
|
||||
users:
|
||||
- stephenfluin
|
||||
- petebacondarwin
|
||||
- gkalpak
|
||||
- IgorMinar
|
||||
- robwormald
|
||||
- wardbell
|
||||
- mhevery #fallback
|
||||
|
23
.travis.yml
23
.travis.yml
@ -1,8 +1,7 @@
|
||||
language: node_js
|
||||
sudo: false
|
||||
dist: trusty
|
||||
node_js:
|
||||
- '8.9.1'
|
||||
- '6.9.5'
|
||||
|
||||
addons:
|
||||
# firefox: "38.0"
|
||||
@ -35,32 +34,28 @@ env:
|
||||
# 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" and "angular-io" firebase projects.
|
||||
# This token was generated using the aio-deploy@angular.io account using `firebase login:ci` and password from valentine
|
||||
- secure: "L5CyQmpwWtoR4Qi4xlWQh/cL1M6ZeJL4W4QAr4HdKFMgYt9h+Whqkymyh2NxwmCbPvWa7yUd+OiLQUDCY7L2VIg16hTwoe2CgYDyQA0BEwLzxtRrJXl93TfwMlrUx5JSIzAccD6D4sjtz8kSFMomK2Nls33xOXOukwyhVMjd0Cg="
|
||||
# ANGULAR_PAYLOAD_FIREBASE_TOKEN
|
||||
# This is for payload size data to "angular-payload-size" firebase project
|
||||
# This token was generated using the payload@angular.io account using `firebase login:ci` and password from valentine
|
||||
- secure: "SxotP/ymNy6uWAVbfwM9BlwETPEBpkRvU/F7fCtQDDic99WfQHzzUSQqHTk8eKk3GrGAOSL09vT0WfStQYEIGEoS5UHWNgOnelxhw+d5EnaoB8vQ0dKQBTK092hQg4feFprr+B/tCasyMV6mVwpUzZMbIJNn/Rx7H5g1bp+Gkfg="
|
||||
# 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="
|
||||
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
|
||||
# deactivated, see #19768
|
||||
# - CI_MODE=browserstack_required
|
||||
- CI_MODE=browserstack_required
|
||||
- CI_MODE=saucelabs_optional
|
||||
- CI_MODE=browserstack_optional
|
||||
- CI_MODE=aio_tools_test
|
||||
- CI_MODE=docs_test
|
||||
- CI_MODE=aio
|
||||
- CI_MODE=aio_e2e AIO_SHARD=0
|
||||
- CI_MODE=aio_e2e AIO_SHARD=1
|
||||
- CI_MODE=aio_e2e
|
||||
|
||||
matrix:
|
||||
fast_finish: true
|
||||
allow_failures:
|
||||
- env: "CI_MODE=saucelabs_optional"
|
||||
- env: "CI_MODE=browserstack_optional"
|
||||
- env: "CI_MODE=aio_e2e"
|
||||
|
||||
before_install:
|
||||
# source the env.sh script so that the exported variables are available to other scripts later on
|
||||
|
59
BUILD.bazel
59
BUILD.bazel
@ -1,59 +0,0 @@
|
||||
package(default_visibility = ["//visibility:public"])
|
||||
|
||||
load("@build_bazel_rules_nodejs//:defs.bzl", "node_modules_filegroup")
|
||||
|
||||
exports_files([
|
||||
"tsconfig.json",
|
||||
"LICENSE",
|
||||
])
|
||||
|
||||
# Developers should always run `bazel run :install`
|
||||
# This ensures that package.json in subdirectories get installed as well.
|
||||
alias(
|
||||
name = "install",
|
||||
actual = "@nodejs//:yarn",
|
||||
)
|
||||
|
||||
node_modules_filegroup(
|
||||
name = "node_modules",
|
||||
packages = [
|
||||
"bytebuffer",
|
||||
"hammerjs",
|
||||
"jasmine",
|
||||
"minimist",
|
||||
"protobufjs",
|
||||
"reflect-metadata",
|
||||
"source-map-support",
|
||||
"tsickle",
|
||||
"tslib",
|
||||
"tsutils",
|
||||
"typescript",
|
||||
"zone.js",
|
||||
"@angular-devkit/core",
|
||||
"@angular-devkit/schematics",
|
||||
"@types",
|
||||
"@webcomponents/custom-elements",
|
||||
],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "web_test_bootstrap_scripts",
|
||||
# do not sort
|
||||
srcs = [
|
||||
"//:node_modules/reflect-metadata/Reflect.js",
|
||||
"//:node_modules/zone.js/dist/zone.js",
|
||||
"//:node_modules/zone.js/dist/zone-testing.js",
|
||||
"//:node_modules/zone.js/dist/task-tracking.js",
|
||||
"//:test-events.js",
|
||||
],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "angularjs_scripts",
|
||||
srcs = [
|
||||
"//:node_modules/angular-1.5/angular.js",
|
||||
"//:node_modules/angular-mocks-1.5/angular-mocks.js",
|
||||
"//:node_modules/angular-mocks/angular-mocks.js",
|
||||
"//:node_modules/angular/angular.js",
|
||||
],
|
||||
)
|
5555
CHANGELOG.md
5555
CHANGELOG.md
File diff suppressed because it is too large
Load Diff
@ -1,12 +0,0 @@
|
||||
# Contributor Code of Conduct
|
||||
## Version 0.3b-angular
|
||||
|
||||
As contributors and maintainers of the Angular project, we pledge to respect everyone who contributes by posting issues, updating documentation, submitting pull requests, providing feedback in comments, and any other activities.
|
||||
|
||||
Communication through any of Angular's channels (GitHub, Gitter, IRC, mailing lists, Google+, Twitter, etc.) must be constructive and never resort to personal attacks, trolling, public or private harassment, insults, or other unprofessional conduct.
|
||||
|
||||
We promise to extend courtesy and respect to everyone involved in this project regardless of gender, gender identity, sexual orientation, disability, age, race, ethnicity, religion, or level of experience. We expect anyone contributing to the Angular project to do the same.
|
||||
|
||||
If any member of the community violates this code of conduct, the maintainers of the Angular project may take action, removing issues, comments, and PRs or blocking accounts as deemed appropriate.
|
||||
|
||||
If you are subject to or witness unacceptable behavior, or have any other concerns, please email us at [conduct@angular.io](mailto:conduct@angular.io).
|
@ -17,15 +17,15 @@ Help us keep Angular open and inclusive. Please read and follow our [Code of Con
|
||||
|
||||
## <a name="question"></a> Got a Question or Problem?
|
||||
|
||||
Do not open issues for general support questions as we want to keep GitHub issues for bug reports and feature requests. You've got much better chances of getting your question answered on [Stack Overflow](https://stackoverflow.com/questions/tagged/angular) where the questions should be tagged with tag `angular`.
|
||||
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`.
|
||||
|
||||
Stack Overflow is a much better place to ask questions since:
|
||||
StackOverflow is a much better place to ask questions since:
|
||||
|
||||
- there are thousands of people willing to help on Stack Overflow
|
||||
- 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
|
||||
- Stack Overflow's voting system assures that the best answers are prominently visible.
|
||||
- StackOverflow's voting system assures that the best answers are prominently visible.
|
||||
|
||||
To save your and our time, we will systematically close all issues that are requests for general support and redirect people to Stack Overflow.
|
||||
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].
|
||||
|
||||
@ -51,7 +51,7 @@ and help you to craft the change so that it is successfully accepted into the pr
|
||||
|
||||
Before you submit an issue, please search the issue tracker, maybe an issue for your problem already exists and the discussion might inform you of workarounds readily available.
|
||||
|
||||
We want to fix all the issues as soon as possible, but before fixing a bug we need to reproduce and confirm it. In order to reproduce bugs, we will systematically ask you to provide a minimal reproduction scenario using http://plnkr.co. Having a live, reproducible scenario gives us a wealth of important information without going back & forth to you with additional questions like:
|
||||
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:
|
||||
|
||||
- version of Angular used
|
||||
- 3rd-party libraries and their versions
|
||||
@ -61,7 +61,7 @@ A minimal reproduce scenario using http://plnkr.co/ allows us to quickly confirm
|
||||
|
||||
We will be insisting on a minimal reproduce scenario in order to save maintainers time and ultimately be able to fix more bugs. Interestingly, from our experience users often find coding problems themselves while preparing a minimal plunk. We understand that sometimes it might be hard to extract essentials bits of code from a larger code-base but we really need to isolate the problem before we can fix it.
|
||||
|
||||
Unfortunately, we are not able to investigate / fix bugs without a minimal reproduction, so if we don't hear back from you we are going to close an issue that doesn't have enough info to be reproduced.
|
||||
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).
|
||||
|
||||
@ -69,37 +69,36 @@ You can file new issues by filling out our [new issue form](https://github.com/a
|
||||
### <a name="submit-pr"></a> Submitting a Pull Request (PR)
|
||||
Before you submit your Pull Request (PR) consider the following guidelines:
|
||||
|
||||
1. Search [GitHub](https://github.com/angular/angular/pulls) for an open or closed PR
|
||||
* Search [GitHub](https://github.com/angular/angular/pulls) for an open or closed PR
|
||||
that relates to your submission. You don't want to duplicate effort.
|
||||
1. Please sign our [Contributor License Agreement (CLA)](#cla) before sending PRs.
|
||||
We cannot accept code without this. Make sure you sign with the primary email address of the Git identity that has been granted access to the Angular repository.
|
||||
1. Fork the angular/angular repo.
|
||||
1. Make your changes in a new git branch:
|
||||
* Please sign our [Contributor License Agreement (CLA)](#cla) before sending PRs.
|
||||
We cannot accept code without this.
|
||||
* Make your changes in a new git branch:
|
||||
|
||||
```shell
|
||||
git checkout -b my-fix-branch master
|
||||
```
|
||||
|
||||
1. Create your patch, **including appropriate test cases**.
|
||||
1. Follow our [Coding Rules](#rules).
|
||||
1. Run the full Angular test suite, as described in the [developer documentation][dev-doc],
|
||||
* Create your patch, **including appropriate test cases**.
|
||||
* Follow our [Coding Rules](#rules).
|
||||
* Run the full Angular test suite, as described in the [developer documentation][dev-doc],
|
||||
and ensure that all tests pass.
|
||||
1. Commit your changes using a descriptive commit message that follows our
|
||||
* Commit your changes using a descriptive commit message that follows our
|
||||
[commit message conventions](#commit). Adherence to these conventions
|
||||
is necessary because release notes are automatically generated from these messages.
|
||||
|
||||
```shell
|
||||
git commit -a
|
||||
```
|
||||
Note: the optional commit `-a` command line option will automatically "add" and "rm" edited files.
|
||||
Note: the optional commit `-a` command line option will automatically "add" and "rm" edited files.
|
||||
|
||||
1. Push your branch to GitHub:
|
||||
* Push your branch to GitHub:
|
||||
|
||||
```shell
|
||||
git push origin my-fix-branch
|
||||
```
|
||||
|
||||
1. In GitHub, send a pull request to `angular:master`.
|
||||
* 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.
|
||||
@ -173,12 +172,12 @@ The **header** is mandatory and the **scope** of the header is optional.
|
||||
Any line of the commit message cannot be longer 100 characters! This allows the message to be easier
|
||||
to read on GitHub as well as in various git tools.
|
||||
|
||||
The footer should contain a [closing reference to an issue](https://help.github.com/articles/closing-issues-via-commit-messages/) if any.
|
||||
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 changelog to beta.5
|
||||
docs(changelog): update change log to beta.5
|
||||
```
|
||||
```
|
||||
fix(release): need to depend on latest rxjs and zone.js
|
||||
@ -199,20 +198,19 @@ Must be one of the following:
|
||||
* **fix**: A bug fix
|
||||
* **perf**: A code change that improves performance
|
||||
* **refactor**: A code change that neither fixes a bug nor adds a feature
|
||||
* **style**: Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc)
|
||||
* **style**: Changes that do not affect the meaning of the code (white-space, formatting, missing
|
||||
semi-colons, etc)
|
||||
* **test**: Adding missing tests or correcting existing tests
|
||||
|
||||
### Scope
|
||||
The scope should be the name of the npm package affected (as perceived by the person reading the changelog generated from commit messages.
|
||||
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:
|
||||
|
||||
* **animations**
|
||||
* **common**
|
||||
* **compiler**
|
||||
* **compiler-cli**
|
||||
* **core**
|
||||
* **elements**
|
||||
* **forms**
|
||||
* **http**
|
||||
* **language-service**
|
||||
@ -222,26 +220,21 @@ The following is the list of supported scopes:
|
||||
* **platform-webworker**
|
||||
* **platform-webworker-dynamic**
|
||||
* **router**
|
||||
* **service-worker**
|
||||
* **upgrade**
|
||||
* **tsc-wrapped**
|
||||
|
||||
There are currently a few exceptions to the "use package name" rule:
|
||||
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.
|
||||
* **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
|
||||
* **docs-infra**: 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`) and for docs changes that are not related to a
|
||||
specific package (e.g. `docs: fix typo in tutorial`).
|
||||
* **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`)
|
||||
|
||||
### Subject
|
||||
The subject contains a succinct description of the change:
|
||||
The subject contains succinct description of the change:
|
||||
|
||||
* use the imperative, present tense: "change" not "changed" nor "changes"
|
||||
* don't capitalize the first letter
|
||||
* don't capitalize first letter
|
||||
* no dot (.) at the end
|
||||
|
||||
### Body
|
||||
@ -265,19 +258,6 @@ changes to be accepted, the CLA must be signed. It's a quick process, we promise
|
||||
* For corporations we'll need you to
|
||||
[print, sign and one of scan+email, fax or mail the form][corporate-cla].
|
||||
|
||||
<hr>
|
||||
|
||||
If you have more than one Git identity, you must make sure that you sign the CLA using the primary email address associated with the ID that has been granted access to the Angular repository. Git identities can be associated with more than one email address, and only one is primary. Here are some links to help you sort out multiple Git identities and email addresses:
|
||||
|
||||
* https://help.github.com/articles/setting-your-commit-email-address-in-git/
|
||||
* https://stackoverflow.com/questions/37245303/what-does-usera-committed-with-userb-13-days-ago-on-github-mean
|
||||
* https://help.github.com/articles/about-commit-email-addresses/
|
||||
* https://help.github.com/articles/blocking-command-line-pushes-that-expose-your-personal-email-address/
|
||||
|
||||
Note that if you have more than one Git identity, it is important to verify that you are logged in with the same ID with which you signed the CLA, before you commit changes. If not, your PR will fail the CLA check.
|
||||
|
||||
<hr>
|
||||
|
||||
|
||||
[angular-group]: https://groups.google.com/forum/#!forum/angular
|
||||
[coc]: https://github.com/angular/code-of-conduct/blob/master/CODE_OF_CONDUCT.md
|
||||
|
2
LICENSE
2
LICENSE
@ -1,6 +1,6 @@
|
||||
The MIT License
|
||||
|
||||
Copyright (c) 2014-2018 Google, Inc. http://angular.io
|
||||
Copyright (c) 2014-2017 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
|
||||
|
27
README.md
27
README.md
@ -1,31 +1,30 @@
|
||||
[](https://travis-ci.org/angular/angular)
|
||||
[](https://circleci.com/gh/angular/angular/tree/master)
|
||||
[](https://www.browserstack.com/automate/public-build/LzF3RzBVVGt6VWE2S0hHaC9uYllOZz09LS1BVjNTclBKV0x4eVRlcjA4QVY1M0N3PT0=--eb4ce8c8dc2c1c5b2b5352d473ee12a73ac20e06)
|
||||
[](https://gitter.im/angular/angular?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
||||
[](https://www.npmjs.com/@angular/core)
|
||||
[](http://issuestats.com/github/angular/angular)
|
||||
[](http://issuestats.com/github/angular/angular)
|
||||
[](https://badge.fury.io/js/%40angular%2Fcore)
|
||||
|
||||
[](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 using Typescript/JavaScript and other languages.
|
||||
|
||||
## Quickstart
|
||||
|
||||
[Get started in 5 minutes][quickstart].
|
||||
|
||||
|
||||
## Changelog
|
||||
|
||||
[Learn about the latest improvements][changelog].
|
||||
|
||||
|
||||
## Want to help?
|
||||
|
||||
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/automate/public-build/LzF3RzBVVGt6VWE2S0hHaC9uYllOZz09LS1BVjNTclBKV0x4eVRlcjA4QVY1M0N3PT0=--eb4ce8c8dc2c1c5b2b5352d473ee12a73ac20e06
|
||||
[contributing]: https://github.com/angular/angular/blob/master/CONTRIBUTING.md
|
||||
[quickstart]: https://angular.io/guide/quickstart
|
||||
[changelog]: https://github.com/angular/angular/blob/master/CHANGELOG.md
|
||||
[ng]: https://angular.io
|
||||
[browserstack]: https://www.browserstack.com/
|
||||
[contributing]: http://github.com/angular/angular/blob/master/CONTRIBUTING.md
|
||||
[quickstart]: https://angular.io/docs/ts/latest/quickstart.html
|
||||
[ng]: http://angular.io
|
||||
|
132
WORKSPACE
132
WORKSPACE
@ -1,132 +0,0 @@
|
||||
workspace(name = "angular")
|
||||
|
||||
#
|
||||
# Download Bazel toolchain dependencies as needed by build actions
|
||||
#
|
||||
|
||||
http_archive(
|
||||
name = "build_bazel_rules_nodejs",
|
||||
url = "https://github.com/bazelbuild/rules_nodejs/archive/0.9.1.zip",
|
||||
strip_prefix = "rules_nodejs-0.9.1",
|
||||
sha256 = "6139762b62b37c1fd171d7f22aa39566cb7dc2916f0f801d505a9aaf118c117f",
|
||||
)
|
||||
|
||||
http_archive(
|
||||
name = "io_bazel_rules_webtesting",
|
||||
url = "https://github.com/bazelbuild/rules_webtesting/archive/v0.2.0.zip",
|
||||
strip_prefix = "rules_webtesting-0.2.0",
|
||||
sha256 = "cecc12f07e95740750a40d38e8b14b76fefa1551bef9332cb432d564d693723c",
|
||||
)
|
||||
|
||||
http_archive(
|
||||
name = "build_bazel_rules_typescript",
|
||||
url = "https://github.com/bazelbuild/rules_typescript/archive/0.15.0.zip",
|
||||
strip_prefix = "rules_typescript-0.15.0",
|
||||
sha256 = "1aa75917330b820cb239b3c10a936a10f0a46fe215063d4492dd76dc6e1616f4",
|
||||
)
|
||||
|
||||
http_archive(
|
||||
name = "io_bazel_rules_go",
|
||||
url = "https://github.com/bazelbuild/rules_go/releases/download/0.10.3/rules_go-0.10.3.tar.gz",
|
||||
sha256 = "feba3278c13cde8d67e341a837f69a029f698d7a27ddbb2a202be7a10b22142a",
|
||||
)
|
||||
|
||||
# This commit matches the version of buildifier in angular/ngcontainer
|
||||
# If you change this, also check if it matches the version in the angular/ngcontainer
|
||||
# version in /.circleci/config.yml
|
||||
BAZEL_BUILDTOOLS_VERSION = "82b21607e00913b16fe1c51bec80232d9d6de31c"
|
||||
|
||||
http_archive(
|
||||
name = "com_github_bazelbuild_buildtools",
|
||||
url = "https://github.com/bazelbuild/buildtools/archive/%s.zip" % BAZEL_BUILDTOOLS_VERSION,
|
||||
strip_prefix = "buildtools-%s" % BAZEL_BUILDTOOLS_VERSION,
|
||||
sha256 = "edb24c2f9c55b10a820ec74db0564415c0cf553fa55e9fc709a6332fb6685eff",
|
||||
)
|
||||
|
||||
# Fetching the Bazel source code allows us to compile the Skylark linter
|
||||
http_archive(
|
||||
name = "io_bazel",
|
||||
url = "https://github.com/bazelbuild/bazel/archive/968f87900dce45a7af749a965b72dbac51b176b3.zip",
|
||||
strip_prefix = "bazel-968f87900dce45a7af749a965b72dbac51b176b3",
|
||||
sha256 = "e373d2ae24955c1254c495c9c421c009d88966565c35e4e8444c082cb1f0f48f",
|
||||
)
|
||||
|
||||
# We have a source dependency on the Devkit repository, because it's built with
|
||||
# Bazel.
|
||||
# This allows us to edit sources and have the effect appear immediately without
|
||||
# re-packaging or "npm link"ing.
|
||||
# Even better, things like aspects will visit the entire graph including
|
||||
# ts_library rules in the devkit repository.
|
||||
http_archive(
|
||||
name = "angular_devkit",
|
||||
url = "https://github.com/angular/devkit/archive/v0.3.1.zip",
|
||||
strip_prefix = "devkit-0.3.1",
|
||||
sha256 = "31d4b597fe9336650acf13df053c1c84dcbe9c29c6a833bcac3819cd3fd8cad3",
|
||||
)
|
||||
|
||||
http_archive(
|
||||
name = "org_brotli",
|
||||
url = "https://github.com/google/brotli/archive/f9b8c02673c576a3e807edbf3a9328e9e7af6d7c.zip",
|
||||
strip_prefix = "brotli-f9b8c02673c576a3e807edbf3a9328e9e7af6d7c",
|
||||
sha256 = "8a517806d2b7c8505ba5c53934e7d7c70d341b68ffd268e9044d35b564a48828",
|
||||
)
|
||||
|
||||
#
|
||||
# Load and install our dependencies downloaded above.
|
||||
#
|
||||
|
||||
load("@build_bazel_rules_nodejs//:defs.bzl", "check_bazel_version", "node_repositories", "yarn_install")
|
||||
|
||||
check_bazel_version("0.14.0")
|
||||
node_repositories(package_json = ["//:package.json"])
|
||||
|
||||
load("@io_bazel_rules_go//go:def.bzl", "go_rules_dependencies", "go_register_toolchains")
|
||||
|
||||
go_rules_dependencies()
|
||||
go_register_toolchains()
|
||||
|
||||
load("@io_bazel_rules_webtesting//web:repositories.bzl", "browser_repositories", "web_test_repositories")
|
||||
|
||||
web_test_repositories()
|
||||
browser_repositories(
|
||||
chromium = True,
|
||||
firefox = True,
|
||||
)
|
||||
|
||||
load("@build_bazel_rules_typescript//:defs.bzl", "ts_setup_workspace")
|
||||
|
||||
ts_setup_workspace()
|
||||
|
||||
#
|
||||
# Point Bazel to WORKSPACEs that live in subdirectories
|
||||
#
|
||||
|
||||
local_repository(
|
||||
name = "rxjs",
|
||||
path = "node_modules/rxjs/src",
|
||||
)
|
||||
|
||||
# Point to the integration test workspace just so that Bazel doesn't descend into it
|
||||
# when expanding the //... pattern
|
||||
local_repository(
|
||||
name = "bazel_integration_test",
|
||||
path = "integration/bazel",
|
||||
)
|
||||
|
||||
#
|
||||
# Ask Bazel to manage these toolchain dependencies for us.
|
||||
# Bazel will run `yarn install` when one of these toolchains is requested during
|
||||
# a build.
|
||||
#
|
||||
|
||||
yarn_install(
|
||||
name = "ts-api-guardian_runtime_deps",
|
||||
package_json = "//tools/ts-api-guardian:package.json",
|
||||
yarn_lock = "//tools/ts-api-guardian:yarn.lock",
|
||||
)
|
||||
|
||||
yarn_install(
|
||||
name = "http-server_runtime_deps",
|
||||
package_json = "//tools/http-server:package.json",
|
||||
yarn_lock = "//tools/http-server:yarn.lock",
|
||||
)
|
65
aio/.angular-cli.json
Normal file
65
aio/.angular-cli.json
Normal file
@ -0,0 +1,65 @@
|
||||
{
|
||||
"$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
|
||||
}
|
||||
}
|
||||
}
|
5
aio/.firebaserc
Normal file
5
aio/.firebaserc
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"projects": {
|
||||
"staging": "aio-staging"
|
||||
}
|
||||
}
|
6
aio/.gitignore
vendored
6
aio/.gitignore
vendored
@ -3,7 +3,7 @@
|
||||
# compiled output
|
||||
/dist
|
||||
/out-tsc
|
||||
/src/generated
|
||||
/src/content
|
||||
/tmp
|
||||
|
||||
# dependencies
|
||||
@ -30,7 +30,6 @@
|
||||
/connect.lock
|
||||
/coverage
|
||||
/libpeerconnection.log
|
||||
debug.log
|
||||
npm-debug.log
|
||||
testem.log
|
||||
/typings
|
||||
@ -44,6 +43,3 @@ protractor-results*.txt
|
||||
# System Files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# copied dependencies
|
||||
src/assets/js/lunr*
|
||||
|
@ -4,7 +4,7 @@ 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 live-example and zip files from the examples.
|
||||
* the tooling for setting up examples for development; and generating plunkers and zip files from the examples.
|
||||
|
||||
## Developer tasks
|
||||
|
||||
@ -13,17 +13,11 @@ You should run all these tasks from the `angular/aio` folder.
|
||||
Here are the most important tasks you might need to use:
|
||||
|
||||
* `yarn` - install all the dependencies.
|
||||
* `yarn setup` - install all the dependencies, boilerplate, stackblitz, zips and run dgeni on the docs.
|
||||
* `yarn setup-local` - same as `setup`, but use the locally built Angular packages for aio and docs examples boilerplate.
|
||||
|
||||
* `yarn build` - create a production build of the application (after installing dependencies, boilerplate, etc).
|
||||
* `yarn build-local` - same as `build`, but use `setup-local` instead of `setup`.
|
||||
* `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 serve-and-sync` - run both the `docs-watch` and `start` in the same console.
|
||||
* `yarn lint` - check that the doc-viewer code follows our style rules.
|
||||
* `yarn test` - run all the unit tests once.
|
||||
* `yarn test --watch` - watch all the source files, for the doc-viewer, and run all the unit tests when any change.
|
||||
* `yarn test` - watch all the source files, for the doc-viewer, and run all the unit tests when any change.
|
||||
* `yarn e2e` - run all the e2e tests for the doc-viewer.
|
||||
|
||||
* `yarn docs` - generate all the docs from the source files.
|
||||
@ -31,33 +25,15 @@ Here are the most important tasks you might need to use:
|
||||
* `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. Add the option `--local` to use your local version of Angular contained in the "dist" folder.
|
||||
* `yarn boilerplate:add` - generate all the boilerplate code for the examples, so that they can be run locally.
|
||||
* `yarn boilerplate:remove` - remove all the boilerplate code that was added via `yarn boilerplate:add`.
|
||||
* `yarn generate-stackblitz` - generate the stackblitz files that are used by the `live-example` tags in the docs.
|
||||
* `yarn generate-plunkers` - generate the plunker files that are used by the `live-example` tags in the docs.
|
||||
* `yarn generate-zips` - generate the zip files from the examples. Zip available via the `live-example` tags in the docs.
|
||||
|
||||
* `yarn example-e2e` - run all e2e tests for examples
|
||||
- `yarn example-e2e --setup` - force webdriver update & other setup, then run tests
|
||||
- `yarn example-e2e --filter=foo` - limit e2e tests to those containing the word "foo"
|
||||
- `yarn example-e2e --setup --local` - run e2e tests with the local version of Angular contained in the "dist" folder
|
||||
|
||||
* `yarn build-ie-polyfills` - generates a js file of polyfills that can be loaded in Internet Explorer.
|
||||
|
||||
## Using ServiceWorker locally
|
||||
|
||||
Since abb36e3cb, running `yarn start --prod` will no longer set up the ServiceWorker, which
|
||||
would require manually running `yarn sw-manifest` and `yarn sw-copy` (something that is not possible
|
||||
with webpack serving the files from memory).
|
||||
|
||||
If you want to test ServiceWorker locally, you can use `yarn build` and serve the files in `dist/`
|
||||
with `yarn http-server dist -p 4200`.
|
||||
|
||||
For more details see #16745.
|
||||
|
||||
|
||||
## Guide to authoring
|
||||
|
||||
There are two types of content in the documentation:
|
||||
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.
|
||||
@ -69,16 +45,8 @@ The content is written in markdown.
|
||||
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.
|
||||
|
||||
* **Code examples**: code examples need to be testable to ensure their accuracy.
|
||||
Also, our examples have a specific look and feel and allow the user to copy the source code. For larger
|
||||
examples they are rendered in a tabbed interface (e.g. template, HTML, and TypeScript on separate
|
||||
tabs). Additionally, some are live examples, which provide links where the code can be edited, executed, and/or downloaded. For details on working with code examples, please read the [Code snippets](https://angular.io/guide/docs-style-guide#code-snippets), [Source code markup](https://angular.io/guide/docs-style-guide#source-code-markup), and [Live examples](https://angular.io/guide/docs-style-guide#live-examples) pages of the [Authors Style Guide](https://angular.io/guide/docs-style-guide).
|
||||
|
||||
We use the [dgeni](https://github.com/angular/dgeni) tool to convert these files into docs that can be viewed in the doc-viewer.
|
||||
|
||||
The [Authors Style Guide](https://angular.io/guide/docs-style-guide) prescribes guidelines for
|
||||
writing guide pages, explains how to use the documentation classes and components, and how to markup sample source code to produce code snippets.
|
||||
|
||||
### Generating the complete docs
|
||||
|
||||
The main task for generating the docs is `yarn docs`. This will process all the source files (API and other),
|
||||
@ -88,7 +56,7 @@ extracting the documentation and generating JSON files that can be consumed by t
|
||||
|
||||
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:
|
||||
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
|
||||
@ -106,7 +74,8 @@ The general setup is as follows:
|
||||
* Open a terminal, ensure the dependencies are installed; run an initial doc generation; then start the doc-viewer:
|
||||
|
||||
```bash
|
||||
yarn setup
|
||||
yarn
|
||||
yarn docs
|
||||
yarn start
|
||||
```
|
||||
|
||||
@ -116,16 +85,8 @@ yarn start
|
||||
yarn docs-watch
|
||||
```
|
||||
|
||||
>Alternatively, try the consolidated `serve-and-sync` command that builds, watches and serves in the same terminal window
|
||||
```bash
|
||||
yarn serve-and-sync
|
||||
```
|
||||
|
||||
* Open a browser at https://localhost:4200/ and navigate to the document on which you want to work.
|
||||
You can automatically open the browser by using `yarn start -o` in the first terminal.
|
||||
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.
|
||||
|
||||
* If you get a build error complaining about examples or any other odd behavior, be sure to consult
|
||||
the [Authors Style Guide](https://angular.io/guide/docs-style-guide).
|
||||
|
@ -19,8 +19,8 @@ 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=team,aio-contributors
|
||||
ARG TEST_AIO_GITHUB_TEAM_SLUGS=team,aio-contributors
|
||||
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
|
||||
@ -29,8 +29,6 @@ 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_TRUSTED_PR_LABEL="aio: preview"
|
||||
ARG TEST_AIO_TRUSTED_PR_LABEL="aio: preview"
|
||||
ARG AIO_UPLOAD_HOSTNAME=upload.localhost
|
||||
ARG TEST_AIO_UPLOAD_HOSTNAME=upload.localhost
|
||||
ARG AIO_UPLOAD_MAX_SIZE=20971520
|
||||
@ -50,11 +48,9 @@ ENV AIO_BUILDS_DIR=$AIO_BUILDS_DIR TEST_AIO_BUILDS_DIR=$TEST
|
||||
AIO_REPO_SLUG=$AIO_REPO_SLUG TEST_AIO_REPO_SLUG=$TEST_AIO_REPO_SLUG \
|
||||
AIO_SCRIPTS_JS_DIR=/usr/share/aio-scripts-js \
|
||||
AIO_SCRIPTS_SH_DIR=/usr/share/aio-scripts-sh \
|
||||
AIO_TRUSTED_PR_LABEL=$AIO_TRUSTED_PR_LABEL TEST_AIO_TRUSTED_PR_LABEL=$TEST_AIO_TRUSTED_PR_LABEL \
|
||||
AIO_UPLOAD_HOSTNAME=$AIO_UPLOAD_HOSTNAME TEST_AIO_UPLOAD_HOSTNAME=$TEST_AIO_UPLOAD_HOSTNAME \
|
||||
AIO_UPLOAD_MAX_SIZE=$AIO_UPLOAD_MAX_SIZE TEST_AIO_UPLOAD_MAX_SIZE=$TEST_AIO_UPLOAD_MAX_SIZE \
|
||||
AIO_UPLOAD_PORT=$AIO_UPLOAD_PORT TEST_AIO_UPLOAD_PORT=$TEST_AIO_UPLOAD_PORT \
|
||||
AIO_WWW_USER=www-data \
|
||||
NODE_ENV=production
|
||||
|
||||
|
||||
@ -67,7 +63,6 @@ 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
|
||||
RUN echo "deb http://ftp.debian.org/debian jessie-backports main" | tee /etc/apt/sources.list.d/backports.list
|
||||
|
||||
|
||||
# Install packages
|
||||
@ -76,11 +71,11 @@ RUN apt-get update -y && apt-get install -y \
|
||||
cron \
|
||||
dnsmasq \
|
||||
nano \
|
||||
nginx \
|
||||
nodejs \
|
||||
openssl \
|
||||
rsyslog \
|
||||
yarn
|
||||
RUN apt-get install -t jessie-backports -y nginx
|
||||
RUN yarn global add pm2@2
|
||||
|
||||
|
||||
@ -114,31 +109,31 @@ RUN update-ca-certificates
|
||||
|
||||
|
||||
# Set up nginx (for production and testing)
|
||||
RUN sed -i -E "s|^user\s+\S+;|user $AIO_WWW_USER;|" /etc/nginx/nginx.conf
|
||||
RUN rm -f /etc/nginx/conf.d/*
|
||||
RUN rm -f /etc/nginx/sites-enabled/*
|
||||
RUN rm /etc/nginx/sites-enabled/*
|
||||
|
||||
COPY nginx/aio-builds.conf /etc/nginx/conf.d/aio-builds-prod.conf
|
||||
RUN sed -i "s|{{\$AIO_BUILDS_DIR}}|$AIO_BUILDS_DIR|g" /etc/nginx/conf.d/aio-builds-prod.conf
|
||||
RUN sed -i "s|{{\$AIO_DOMAIN_NAME}}|$AIO_DOMAIN_NAME|g" /etc/nginx/conf.d/aio-builds-prod.conf
|
||||
RUN sed -i "s|{{\$AIO_LOCALCERTS_DIR}}|$AIO_LOCALCERTS_DIR|g" /etc/nginx/conf.d/aio-builds-prod.conf
|
||||
RUN sed -i "s|{{\$AIO_NGINX_LOGS_DIR}}|$AIO_NGINX_LOGS_DIR|g" /etc/nginx/conf.d/aio-builds-prod.conf
|
||||
RUN sed -i "s|{{\$AIO_NGINX_PORT_HTTP}}|$AIO_NGINX_PORT_HTTP|g" /etc/nginx/conf.d/aio-builds-prod.conf
|
||||
RUN sed -i "s|{{\$AIO_NGINX_PORT_HTTPS}}|$AIO_NGINX_PORT_HTTPS|g" /etc/nginx/conf.d/aio-builds-prod.conf
|
||||
RUN sed -i "s|{{\$AIO_UPLOAD_HOSTNAME}}|$AIO_UPLOAD_HOSTNAME|g" /etc/nginx/conf.d/aio-builds-prod.conf
|
||||
RUN sed -i "s|{{\$AIO_UPLOAD_MAX_SIZE}}|$AIO_UPLOAD_MAX_SIZE|g" /etc/nginx/conf.d/aio-builds-prod.conf
|
||||
RUN sed -i "s|{{\$AIO_UPLOAD_PORT}}|$AIO_UPLOAD_PORT|g" /etc/nginx/conf.d/aio-builds-prod.conf
|
||||
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/conf.d/aio-builds-test.conf
|
||||
RUN sed -i "s|{{\$AIO_BUILDS_DIR}}|$TEST_AIO_BUILDS_DIR|g" /etc/nginx/conf.d/aio-builds-test.conf
|
||||
RUN sed -i "s|{{\$AIO_DOMAIN_NAME}}|$TEST_AIO_DOMAIN_NAME|g" /etc/nginx/conf.d/aio-builds-test.conf
|
||||
RUN sed -i "s|{{\$AIO_LOCALCERTS_DIR}}|$TEST_AIO_LOCALCERTS_DIR|g" /etc/nginx/conf.d/aio-builds-test.conf
|
||||
RUN sed -i "s|{{\$AIO_NGINX_LOGS_DIR}}|$TEST_AIO_NGINX_LOGS_DIR|g" /etc/nginx/conf.d/aio-builds-test.conf
|
||||
RUN sed -i "s|{{\$AIO_NGINX_PORT_HTTP}}|$TEST_AIO_NGINX_PORT_HTTP|g" /etc/nginx/conf.d/aio-builds-test.conf
|
||||
RUN sed -i "s|{{\$AIO_NGINX_PORT_HTTPS}}|$TEST_AIO_NGINX_PORT_HTTPS|g" /etc/nginx/conf.d/aio-builds-test.conf
|
||||
RUN sed -i "s|{{\$AIO_UPLOAD_HOSTNAME}}|$TEST_AIO_UPLOAD_HOSTNAME|g" /etc/nginx/conf.d/aio-builds-test.conf
|
||||
RUN sed -i "s|{{\$AIO_UPLOAD_MAX_SIZE}}|$TEST_AIO_UPLOAD_MAX_SIZE|g" /etc/nginx/conf.d/aio-builds-test.conf
|
||||
RUN sed -i "s|{{\$AIO_UPLOAD_PORT}}|$TEST_AIO_UPLOAD_PORT|g" /etc/nginx/conf.d/aio-builds-test.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
|
||||
@ -156,7 +151,7 @@ RUN find $AIO_SCRIPTS_SH_DIR -maxdepth 1 -type f -printf "%P\n" \
|
||||
# Set up the Node.js scripts
|
||||
COPY scripts-js/ $AIO_SCRIPTS_JS_DIR/
|
||||
WORKDIR $AIO_SCRIPTS_JS_DIR/
|
||||
RUN yarn install --production --frozen-lockfile
|
||||
RUN yarn install --production
|
||||
|
||||
|
||||
# Set up health check
|
||||
|
@ -6,7 +6,7 @@ server=8.8.4.4
|
||||
# Listen for DHCP and DNS requests only on this address.
|
||||
listen-address=127.0.0.1
|
||||
|
||||
# Force an IP address for these domains.
|
||||
# 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
|
||||
|
@ -15,24 +15,18 @@ server {
|
||||
|
||||
# Serve PR-preview requests
|
||||
server {
|
||||
server_name "~^pr(?<pr>[1-9][0-9]*)-(?<sha>[0-9a-f]{7,40})\.";
|
||||
server_name "~^pr(?<pr>[1-9][0-9]*)-(?<sha>[0-9a-f]{40})\.";
|
||||
|
||||
listen {{$AIO_NGINX_PORT_HTTPS}} ssl http2;
|
||||
listen [::]:{{$AIO_NGINX_PORT_HTTPS}} ssl http2;
|
||||
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;
|
||||
ssl_prefer_server_ciphers on;
|
||||
ssl_ciphers EECDH+CHACHA20:EECDH+AES128:RSA+AES128:EECDH+AES256:RSA+AES256:EECDH+3DES:RSA+3DES:!MD5;
|
||||
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;
|
||||
|
||||
gzip on;
|
||||
gzip_comp_level 7;
|
||||
gzip_types *;
|
||||
|
||||
access_log {{$AIO_NGINX_LOGS_DIR}}/access.log;
|
||||
error_log {{$AIO_NGINX_LOGS_DIR}}/error.log;
|
||||
|
||||
@ -49,13 +43,11 @@ server {
|
||||
server {
|
||||
server_name _;
|
||||
|
||||
listen {{$AIO_NGINX_PORT_HTTPS}} ssl http2 default_server;
|
||||
listen [::]:{{$AIO_NGINX_PORT_HTTPS}} ssl http2;
|
||||
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;
|
||||
ssl_prefer_server_ciphers on;
|
||||
ssl_ciphers EECDH+CHACHA20:EECDH+AES128:RSA+AES128:EECDH+AES256:RSA+AES256:EECDH+3DES:RSA+3DES:!MD5;
|
||||
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;
|
||||
@ -88,21 +80,6 @@ server {
|
||||
resolver 127.0.0.1;
|
||||
}
|
||||
|
||||
# Notify about PR changes
|
||||
location "~^/pr-updated/?$" {
|
||||
if ($request_method != "POST") {
|
||||
add_header Allow "POST";
|
||||
return 405;
|
||||
}
|
||||
|
||||
proxy_pass_request_headers on;
|
||||
proxy_redirect off;
|
||||
proxy_method POST;
|
||||
proxy_pass http://{{$AIO_UPLOAD_HOSTNAME}}:{{$AIO_UPLOAD_PORT}}$request_uri;
|
||||
|
||||
resolver 127.0.0.1;
|
||||
}
|
||||
|
||||
# Everything else
|
||||
location / {
|
||||
return 404;
|
||||
|
@ -2,7 +2,6 @@
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as shell from 'shelljs';
|
||||
import {HIDDEN_DIR_PREFIX} from '../common/constants';
|
||||
import {GithubPullRequests} from '../common/github-pull-requests';
|
||||
import {assertNotMissingOrEmpty} from '../common/utils';
|
||||
|
||||
@ -32,9 +31,8 @@ export class BuildCleaner {
|
||||
}
|
||||
|
||||
const buildNumbers = files.
|
||||
map(name => name.replace(HIDDEN_DIR_PREFIX, '')). // Remove the "hidden dir" prefix
|
||||
map(Number). // Convert string to number
|
||||
filter(Boolean); // Ignore NaN (or 0), because they are not builds
|
||||
map(Number). // Convert string to number
|
||||
filter(Boolean); // Ignore NaN (or 0), because they are not builds
|
||||
|
||||
resolve(buildNumbers);
|
||||
});
|
||||
@ -51,10 +49,9 @@ export class BuildCleaner {
|
||||
|
||||
protected removeDir(dir: string) {
|
||||
try {
|
||||
if (shell.test('-d', dir)) {
|
||||
shell.chmod('-R', 'a+w', dir);
|
||||
shell.rm('-rf', dir);
|
||||
}
|
||||
// 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);
|
||||
}
|
||||
@ -67,14 +64,8 @@ export class BuildCleaner {
|
||||
console.log(`Open pull requests: ${openPrNumbers.length}`);
|
||||
console.log(`Removing ${toRemove.length} build(s): ${toRemove.join(', ')}`);
|
||||
|
||||
// Try removing public dirs.
|
||||
toRemove.
|
||||
map(num => path.join(this.buildsDir, String(num))).
|
||||
forEach(dir => this.removeDir(dir));
|
||||
|
||||
// Try removing hidden dirs.
|
||||
toRemove.
|
||||
map(num => path.join(this.buildsDir, HIDDEN_DIR_PREFIX + String(num))).
|
||||
forEach(dir => this.removeDir(dir));
|
||||
}
|
||||
}
|
||||
|
@ -1,3 +0,0 @@
|
||||
// Constants
|
||||
export const HIDDEN_DIR_PREFIX = 'hidden--';
|
||||
export const SHORT_SHA_LEN = 7;
|
@ -63,7 +63,7 @@ export class GithubApi {
|
||||
return items;
|
||||
}
|
||||
|
||||
return this.getPaginated<T>(pathname, baseParams, currentPage + 1).then(moreItems => [...items, ...moreItems]);
|
||||
return this.getPaginated(pathname, baseParams, currentPage + 1).then(moreItems => [...items, ...moreItems]);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -6,7 +6,6 @@ import {GithubApi} from './github-api';
|
||||
export interface PullRequest {
|
||||
number: number;
|
||||
user: {login: string};
|
||||
labels: {name: string}[];
|
||||
}
|
||||
|
||||
export type PullRequestState = 'all' | 'closed' | 'open';
|
||||
@ -31,8 +30,7 @@ export class GithubPullRequests extends GithubApi {
|
||||
}
|
||||
|
||||
public fetch(pr: number): Promise<PullRequest> {
|
||||
// Using the `/issues/` URL, because the `/pulls/` one does not provide labels.
|
||||
return this.get<PullRequest>(`/repos/${this.repoSlug}/issues/${pr}`);
|
||||
return this.get<PullRequest>(`/repos/${this.repoSlug}/pulls/${pr}`);
|
||||
}
|
||||
|
||||
public fetchAll(state: PullRequestState = 'all'): Promise<PullRequest[]> {
|
||||
|
@ -4,9 +4,8 @@ import {EventEmitter} from 'events';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as shell from 'shelljs';
|
||||
import {HIDDEN_DIR_PREFIX, SHORT_SHA_LEN} from '../common/constants';
|
||||
import {assertNotMissingOrEmpty} from '../common/utils';
|
||||
import {ChangedPrVisibilityEvent, CreatedBuildEvent} from './build-events';
|
||||
import {CreatedBuildEvent} from './build-events';
|
||||
import {UploadError} from './upload-error';
|
||||
|
||||
// Classes
|
||||
@ -18,22 +17,16 @@ export class BuildCreator extends EventEmitter {
|
||||
}
|
||||
|
||||
// Methods - Public
|
||||
public create(pr: string, sha: string, archivePath: string, isPublic: boolean): Promise<void> {
|
||||
// Use only part of the SHA for more readable URLs.
|
||||
sha = sha.substr(0, SHORT_SHA_LEN);
|
||||
|
||||
const {newPrDir: prDir} = this.getCandidatePrDirs(pr, isPublic);
|
||||
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.resolve().
|
||||
// If the same PR exists with different visibility, update the visibility first.
|
||||
then(() => this.updatePrVisibility(pr, isPublic)).
|
||||
then(() => Promise.all([this.exists(prDir), this.exists(shaDir)])).
|
||||
return Promise.
|
||||
all([this.exists(prDir), this.exists(shaDir)]).
|
||||
then(([prDirExisted, shaDirExisted]) => {
|
||||
if (shaDirExisted) {
|
||||
const publicOrNot = isPublic ? 'public' : 'non-public';
|
||||
throw new UploadError(409, `Request to overwrite existing ${publicOrNot} directory: ${shaDir}`);
|
||||
throw new UploadError(409, `Request to overwrite existing directory: ${shaDir}`);
|
||||
}
|
||||
|
||||
dirToRemoveOnError = prDirExisted ? shaDir : prDir;
|
||||
@ -41,8 +34,7 @@ export class BuildCreator extends EventEmitter {
|
||||
return Promise.resolve().
|
||||
then(() => shell.mkdir('-p', shaDir)).
|
||||
then(() => this.extractArchive(archivePath, shaDir)).
|
||||
then(() => this.emit(CreatedBuildEvent.type, new CreatedBuildEvent(+pr, sha, isPublic))).
|
||||
then(() => undefined);
|
||||
then(() => this.emit(CreatedBuildEvent.type, new CreatedBuildEvent(+pr, sha)));
|
||||
}).
|
||||
catch(err => {
|
||||
if (dirToRemoveOnError) {
|
||||
@ -57,36 +49,6 @@ export class BuildCreator extends EventEmitter {
|
||||
});
|
||||
}
|
||||
|
||||
public updatePrVisibility(pr: string, makePublic: boolean): Promise<boolean> {
|
||||
const {oldPrDir: otherVisPrDir, newPrDir: targetVisPrDir} = this.getCandidatePrDirs(pr, makePublic);
|
||||
|
||||
return Promise.
|
||||
all([this.exists(otherVisPrDir), this.exists(targetVisPrDir)]).
|
||||
then(([otherVisPrDirExisted, targetVisPrDirExisted]) => {
|
||||
if (!otherVisPrDirExisted) {
|
||||
// No visibility change: Either the visibility is up-to-date or the PR does not exist.
|
||||
return false;
|
||||
} else if (targetVisPrDirExisted) {
|
||||
// Error: Directories for both visibilities exist.
|
||||
throw new UploadError(409, `Request to move '${otherVisPrDir}' to existing directory '${targetVisPrDir}'.`);
|
||||
}
|
||||
|
||||
// Visibility change: Moving `otherVisPrDir` to `targetVisPrDir`.
|
||||
return Promise.resolve().
|
||||
then(() => shell.mv(otherVisPrDir, targetVisPrDir)).
|
||||
then(() => this.listShasByDate(targetVisPrDir)).
|
||||
then(shas => this.emit(ChangedPrVisibilityEvent.type, new ChangedPrVisibilityEvent(+pr, shas, makePublic))).
|
||||
then(() => true);
|
||||
}).
|
||||
catch(err => {
|
||||
if (!(err instanceof UploadError)) {
|
||||
err = new UploadError(500, `Error while making PR ${pr} ${makePublic ? 'public' : 'hidden'}.\n${err}`);
|
||||
}
|
||||
|
||||
throw err;
|
||||
});
|
||||
}
|
||||
|
||||
// Methods - Protected
|
||||
protected exists(fileOrDir: string): Promise<boolean> {
|
||||
return new Promise(resolve => fs.access(fileOrDir, err => resolve(!err)));
|
||||
@ -106,7 +68,8 @@ export class BuildCreator extends EventEmitter {
|
||||
}
|
||||
|
||||
try {
|
||||
shell.chmod('-R', 'a-w', outputDir);
|
||||
// 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) {
|
||||
@ -115,26 +78,4 @@ export class BuildCreator extends EventEmitter {
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
protected getCandidatePrDirs(pr: string, isPublic: boolean) {
|
||||
const hiddenPrDir = path.join(this.buildsDir, HIDDEN_DIR_PREFIX + pr);
|
||||
const publicPrDir = path.join(this.buildsDir, pr);
|
||||
|
||||
const oldPrDir = isPublic ? hiddenPrDir : publicPrDir;
|
||||
const newPrDir = isPublic ? publicPrDir : hiddenPrDir;
|
||||
|
||||
return {oldPrDir, newPrDir};
|
||||
}
|
||||
|
||||
protected listShasByDate(inputDir: string): Promise<string[]> {
|
||||
return Promise.resolve().
|
||||
then(() => shell.ls('-l', inputDir) as any as Promise<(fs.Stats & {name: string})[]>).
|
||||
// Keep directories only.
|
||||
// (Also, convert to standard Array - ShellJS provides custom `sort()` method for sorting file contents.)
|
||||
then(items => items.filter(item => item.isDirectory())).
|
||||
// Sort by modification date.
|
||||
then(items => items.sort((a, b) => a.mtime.getTime() - b.mtime.getTime())).
|
||||
// Return directory names.
|
||||
then(items => items.map(item => item.name));
|
||||
}
|
||||
}
|
||||
|
@ -1,16 +1,15 @@
|
||||
// Classes
|
||||
export class ChangedPrVisibilityEvent {
|
||||
// Properties - Public, Static
|
||||
public static type = 'pr.changedVisibility';
|
||||
|
||||
export class BuildEvent {
|
||||
// Constructor
|
||||
constructor(public pr: number, public shas: string[], public isPublic: boolean) {}
|
||||
constructor(public type: string, public pr: number, public sha: string) {}
|
||||
}
|
||||
|
||||
export class CreatedBuildEvent {
|
||||
export class CreatedBuildEvent extends BuildEvent {
|
||||
// Properties - Public, Static
|
||||
public static type = 'build.created';
|
||||
|
||||
// Constructor
|
||||
constructor(public pr: number, public sha: string, public isPublic: boolean) {}
|
||||
constructor(pr: number, sha: string) {
|
||||
super(CreatedBuildEvent.type, pr, sha);
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
// Imports
|
||||
import * as jwt from 'jsonwebtoken';
|
||||
import {GithubPullRequests, PullRequest} from '../common/github-pull-requests';
|
||||
import {GithubPullRequests} from '../common/github-pull-requests';
|
||||
import {GithubTeams} from '../common/github-teams';
|
||||
import {assertNotMissingOrEmpty} from '../common/utils';
|
||||
import {UploadError} from './upload-error';
|
||||
@ -11,12 +11,6 @@ interface JwtPayload {
|
||||
'pull-request': number;
|
||||
}
|
||||
|
||||
// Enums
|
||||
export enum BUILD_VERIFICATION_STATUS {
|
||||
verifiedAndTrusted,
|
||||
verifiedNotTrusted,
|
||||
}
|
||||
|
||||
// Classes
|
||||
export class BuildVerifier {
|
||||
// Properties - Protected
|
||||
@ -25,27 +19,27 @@ export class BuildVerifier {
|
||||
|
||||
// Constructor
|
||||
constructor(protected secret: string, githubToken: string, protected repoSlug: string, organization: string,
|
||||
protected allowedTeamSlugs: string[], protected trustedPrLabel: string) {
|
||||
protected allowedTeamSlugs: string[]) {
|
||||
assertNotMissingOrEmpty('secret', secret);
|
||||
assertNotMissingOrEmpty('githubToken', githubToken);
|
||||
assertNotMissingOrEmpty('repoSlug', repoSlug);
|
||||
assertNotMissingOrEmpty('organization', organization);
|
||||
assertNotMissingOrEmpty('allowedTeamSlugs', allowedTeamSlugs && allowedTeamSlugs.join(''));
|
||||
assertNotMissingOrEmpty('trustedPrLabel', trustedPrLabel);
|
||||
|
||||
this.githubPullRequests = new GithubPullRequests(githubToken, repoSlug);
|
||||
this.githubTeams = new GithubTeams(githubToken, organization);
|
||||
}
|
||||
|
||||
// Methods - Public
|
||||
public getPrIsTrusted(pr: number): Promise<boolean> {
|
||||
public getPrAuthorTeamMembership(pr: number): Promise<{author: string, isMember: boolean}> {
|
||||
return Promise.resolve().
|
||||
then(() => this.githubPullRequests.fetch(pr)).
|
||||
then(prInfo => this.hasLabel(prInfo, this.trustedPrLabel) ||
|
||||
this.githubTeams.isMemberBySlug(prInfo.user.login, this.allowedTeamSlugs));
|
||||
then(prInfo => prInfo.user.login).
|
||||
then(author => this.githubTeams.isMemberBySlug(author, this.allowedTeamSlugs).
|
||||
then(isMember => ({author, isMember})));
|
||||
}
|
||||
|
||||
public verify(expectedPr: number, authHeader: string): Promise<BUILD_VERIFICATION_STATUS> {
|
||||
public verify(expectedPr: number, authHeader: string): Promise<void> {
|
||||
return Promise.resolve().
|
||||
then(() => this.extractJwtString(authHeader)).
|
||||
then(jwtString => this.verifyJwt(expectedPr, jwtString)).
|
||||
@ -58,13 +52,9 @@ export class BuildVerifier {
|
||||
return input.replace(/^token +/i, '');
|
||||
}
|
||||
|
||||
protected hasLabel(prInfo: PullRequest, label: string) {
|
||||
return prInfo.labels.some(labelObj => labelObj.name === label);
|
||||
}
|
||||
|
||||
protected verifyJwt(expectedPr: number, token: string): Promise<JwtPayload> {
|
||||
return new Promise((resolve, reject) => {
|
||||
jwt.verify(token, this.secret, {issuer: 'Travis CI, GmbH'}, (err, payload: JwtPayload) => {
|
||||
jwt.verify(token, this.secret, {issuer: 'Travis CI, GmbH'}, (err, payload) => {
|
||||
if (err) {
|
||||
reject(err.message || err);
|
||||
} else if (payload.slug !== this.repoSlug) {
|
||||
@ -78,10 +68,11 @@ export class BuildVerifier {
|
||||
});
|
||||
}
|
||||
|
||||
protected verifyPr(pr: number): Promise<BUILD_VERIFICATION_STATUS> {
|
||||
return this.getPrIsTrusted(pr).
|
||||
then(isTrusted => Promise.resolve(isTrusted ?
|
||||
BUILD_VERIFICATION_STATUS.verifiedAndTrusted :
|
||||
BUILD_VERIFICATION_STATUS.verifiedNotTrusted));
|
||||
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(', ')}`,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
@ -12,28 +12,28 @@ function _main() {
|
||||
const repoSlug = getEnvVar('AIO_REPO_SLUG');
|
||||
const organization = getEnvVar('AIO_GITHUB_ORGANIZATION');
|
||||
const allowedTeamSlugs = getEnvVar('AIO_GITHUB_TEAM_SLUGS').split(',');
|
||||
const trustedPrLabel = getEnvVar('AIO_TRUSTED_PR_LABEL');
|
||||
const pr = +getEnvVar('AIO_PREVERIFY_PR');
|
||||
|
||||
const buildVerifier = new BuildVerifier(secret, githubToken, repoSlug, organization, allowedTeamSlugs,
|
||||
trustedPrLabel);
|
||||
const buildVerifier = new BuildVerifier(secret, githubToken, repoSlug, organization, allowedTeamSlugs);
|
||||
|
||||
// Exit codes:
|
||||
// - 0: The PR can be automatically trusted (i.e. author belongs to trusted team or PR has the "trusted PR" label).
|
||||
// - 1: An error occurred.
|
||||
// - 2: The PR cannot be automatically trusted.
|
||||
buildVerifier.getPrIsTrusted(pr).
|
||||
then(isTrusted => {
|
||||
if (!isTrusted) {
|
||||
console.warn(
|
||||
`The PR cannot be automatically verified, because it doesn't have the "${trustedPrLabel}" label and the ` +
|
||||
`the author is not an active member of any of the following teams: ${allowedTeamSlugs.join(', ')}`);
|
||||
// - 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);
|
||||
}
|
||||
|
||||
process.exit(isTrusted ? 0 : 2);
|
||||
}).
|
||||
catch(err => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
catch(err => onError(err, 2));
|
||||
}
|
||||
|
||||
function onError(err: string, exitCode: number) {
|
||||
console.error(err);
|
||||
process.exit(exitCode || 1);
|
||||
}
|
||||
|
@ -0,0 +1,10 @@
|
||||
// Imports
|
||||
import {GithubPullRequests} from '../common/github-pull-requests';
|
||||
import {BuildVerifier} from './build-verifier';
|
||||
|
||||
// Run
|
||||
// TODO(gkalpak): Add e2e tests to cover these interactions as well.
|
||||
GithubPullRequests.prototype.addComment = () => Promise.resolve();
|
||||
BuildVerifier.prototype.verify = () => Promise.resolve();
|
||||
// tslint:disable-next-line: no-var-requires
|
||||
require('./index');
|
@ -1,3 +1,6 @@
|
||||
// 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';
|
||||
@ -10,7 +13,6 @@ const AIO_GITHUB_TEAM_SLUGS = getEnvVar('AIO_GITHUB_TEAM_SLUGS');
|
||||
const AIO_GITHUB_TOKEN = getEnvVar('AIO_GITHUB_TOKEN');
|
||||
const AIO_PREVIEW_DEPLOYMENT_TOKEN = getEnvVar('AIO_PREVIEW_DEPLOYMENT_TOKEN');
|
||||
const AIO_REPO_SLUG = getEnvVar('AIO_REPO_SLUG');
|
||||
const AIO_TRUSTED_PR_LABEL = getEnvVar('AIO_TRUSTED_PR_LABEL');
|
||||
const AIO_UPLOAD_HOSTNAME = getEnvVar('AIO_UPLOAD_HOSTNAME');
|
||||
const AIO_UPLOAD_PORT = +getEnvVar('AIO_UPLOAD_PORT');
|
||||
|
||||
@ -28,7 +30,6 @@ function _main() {
|
||||
githubToken: AIO_GITHUB_TOKEN,
|
||||
repoSlug: AIO_REPO_SLUG,
|
||||
secret: AIO_PREVIEW_DEPLOYMENT_TOKEN,
|
||||
trustedPrLabel: AIO_TRUSTED_PR_LABEL,
|
||||
}).
|
||||
listen(AIO_UPLOAD_PORT, AIO_UPLOAD_HOSTNAME);
|
||||
}
|
||||
|
@ -1,12 +1,11 @@
|
||||
// Imports
|
||||
import * as bodyParser from 'body-parser';
|
||||
import * as express from 'express';
|
||||
import * as http from 'http';
|
||||
import {GithubPullRequests} from '../common/github-pull-requests';
|
||||
import {assertNotMissingOrEmpty} from '../common/utils';
|
||||
import {BuildCreator} from './build-creator';
|
||||
import {ChangedPrVisibilityEvent, CreatedBuildEvent} from './build-events';
|
||||
import {BUILD_VERIFICATION_STATUS, BuildVerifier} from './build-verifier';
|
||||
import {CreatedBuildEvent} from './build-events';
|
||||
import {BuildVerifier} from './build-verifier';
|
||||
import {UploadError} from './upload-error';
|
||||
|
||||
// Constants
|
||||
@ -22,7 +21,6 @@ interface UploadServerConfig {
|
||||
githubToken: string;
|
||||
repoSlug: string;
|
||||
secret: string;
|
||||
trustedPrLabel: string;
|
||||
}
|
||||
|
||||
// Classes
|
||||
@ -36,16 +34,14 @@ class UploadServerFactory {
|
||||
githubToken,
|
||||
repoSlug,
|
||||
secret,
|
||||
trustedPrLabel,
|
||||
}: UploadServerConfig): http.Server {
|
||||
assertNotMissingOrEmpty('domainName', domainName);
|
||||
|
||||
const buildVerifier = new BuildVerifier(secret, githubToken, repoSlug, githubOrganization, githubTeamSlugs,
|
||||
trustedPrLabel);
|
||||
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 as any);
|
||||
const httpServer = http.createServer(middleware);
|
||||
|
||||
httpServer.on('listening', () => {
|
||||
const info = httpServer.address();
|
||||
@ -60,24 +56,12 @@ class UploadServerFactory {
|
||||
domainName: string): BuildCreator {
|
||||
const buildCreator = new BuildCreator(buildsDir);
|
||||
const githubPullRequests = new GithubPullRequests(githubToken, repoSlug);
|
||||
const postPreviewsComment = (pr: number, shas: string[]) => {
|
||||
const body = shas.
|
||||
map(sha => `You can preview ${sha} at https://pr${pr}-${sha}.${domainName}/.`).
|
||||
join('\n');
|
||||
|
||||
return githubPullRequests.addComment(pr, body);
|
||||
};
|
||||
buildCreator.on(CreatedBuildEvent.type, ({pr, sha}: CreatedBuildEvent) => {
|
||||
const body = `The angular.io preview for ${sha} is available [here][1].\n\n` +
|
||||
`[1]: https://pr${pr}-${sha}.${domainName}/`;
|
||||
|
||||
buildCreator.on(CreatedBuildEvent.type, ({pr, sha, isPublic}: CreatedBuildEvent) => {
|
||||
if (isPublic) {
|
||||
postPreviewsComment(pr, [sha]);
|
||||
}
|
||||
});
|
||||
|
||||
buildCreator.on(ChangedPrVisibilityEvent.type, ({pr, shas, isPublic}: ChangedPrVisibilityEvent) => {
|
||||
if (isPublic && shas.length) {
|
||||
postPreviewsComment(pr, shas);
|
||||
}
|
||||
githubPullRequests.addComment(pr, body);
|
||||
});
|
||||
|
||||
return buildCreator;
|
||||
@ -85,7 +69,6 @@ class UploadServerFactory {
|
||||
|
||||
protected createMiddleware(buildVerifier: BuildVerifier, buildCreator: BuildCreator): express.Express {
|
||||
const middleware = express();
|
||||
const jsonParser = bodyParser.json();
|
||||
|
||||
middleware.get(/^\/create-build\/([1-9][0-9]*)\/([0-9a-f]{40})\/?$/, (req, res) => {
|
||||
const pr = req.params[0];
|
||||
@ -97,33 +80,17 @@ class UploadServerFactory {
|
||||
this.throwRequestError(401, `Missing or empty '${AUTHORIZATION_HEADER}' header`, req);
|
||||
} else if (!archive) {
|
||||
this.throwRequestError(400, `Missing or empty '${X_FILE_HEADER}' header`, req);
|
||||
} else {
|
||||
Promise.resolve().
|
||||
then(() => buildVerifier.verify(+pr, authHeader)).
|
||||
then(verStatus => verStatus === BUILD_VERIFICATION_STATUS.verifiedAndTrusted).
|
||||
then(isPublic => buildCreator.create(pr, sha, archive, isPublic).
|
||||
then(() => res.sendStatus(isPublic ? 201 : 202))).
|
||||
catch(err => this.respondWithError(res, err));
|
||||
}
|
||||
|
||||
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.post(/^\/pr-updated\/?$/, jsonParser, (req, res) => {
|
||||
const {action, number: prNo}: {action?: string, number?: number} = req.body;
|
||||
const visMayHaveChanged = !action || (action === 'labeled') || (action === 'unlabeled');
|
||||
|
||||
if (!visMayHaveChanged) {
|
||||
res.sendStatus(200);
|
||||
} else if (!prNo) {
|
||||
this.throwRequestError(400, `Missing or empty 'number' field`, req);
|
||||
} else {
|
||||
Promise.resolve().
|
||||
then(() => buildVerifier.getPrIsTrusted(prNo)).
|
||||
then(isPublic => buildCreator.updatePrVisibility(String(prNo), isPublic)).
|
||||
then(() => res.sendStatus(200)).
|
||||
catch(err => this.respondWithError(res, err));
|
||||
}
|
||||
});
|
||||
middleware.all('*', req => this.throwRequestError(404, 'Unknown resource', req));
|
||||
middleware.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;
|
||||
@ -142,10 +109,7 @@ class UploadServerFactory {
|
||||
}
|
||||
|
||||
protected throwRequestError(status: number, error: string, req: express.Request) {
|
||||
const message = `${error} in request: ${req.method} ${req.originalUrl}` +
|
||||
(!req.body ? '' : ` ${JSON.stringify(req.body)}`);
|
||||
|
||||
throw new UploadError(status, message);
|
||||
throw new UploadError(status, `${error} in request: ${req.method} ${req.originalUrl}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,16 +0,0 @@
|
||||
// Using the values below, we can fake the response of the corresponding methods in tests. This is
|
||||
// necessary, because the test upload-server will be running as a separate node process, so we will
|
||||
// not have direct access to the code (e.g. for mocking).
|
||||
// (See also 'lib/verify-setup/start-test-upload-server.ts'.)
|
||||
|
||||
/* tslint:disable: variable-name */
|
||||
|
||||
// Special values to be used as `authHeader` in `BuildVerifier#verify()`.
|
||||
export const BV_verify_error = 'FAKE_VERIFICATION_ERROR';
|
||||
export const BV_verify_verifiedNotTrusted = 'FAKE_VERIFIED_NOT_TRUSTED';
|
||||
|
||||
// Special values to be used as `pr` in `BuildVerifier#getPrIsTrusted()`.
|
||||
export const BV_getPrIsTrusted_error = 32203;
|
||||
export const BV_getPrIsTrusted_notTrusted = 72457;
|
||||
|
||||
/* tslint:enable: variable-name */
|
@ -4,10 +4,10 @@ import * as fs from 'fs';
|
||||
import * as http from 'http';
|
||||
import * as path from 'path';
|
||||
import * as shell from 'shelljs';
|
||||
import {HIDDEN_DIR_PREFIX, SHORT_SHA_LEN} from '../common/constants';
|
||||
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');
|
||||
@ -15,10 +15,9 @@ 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');
|
||||
const WWW_USER = getEnvVar('AIO_WWW_USER');
|
||||
|
||||
// Interfaces - Types
|
||||
export interface CmdResult { success: boolean; err: Error | null; stdout: string; stderr: string; }
|
||||
export interface CmdResult { success: boolean; err: Error; stdout: string; stderr: string; }
|
||||
export interface FileSpecs { content?: string; size?: number; }
|
||||
|
||||
export type CleanUpFn = () => void;
|
||||
@ -32,10 +31,10 @@ class Helper {
|
||||
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; }
|
||||
public get wwwUser() { return WWW_USER; }
|
||||
|
||||
// Properties - Protected
|
||||
protected cleanUpFns: CleanUpFn[] = [];
|
||||
@ -47,16 +46,10 @@ class Helper {
|
||||
// Constructor
|
||||
constructor() {
|
||||
shell.mkdir('-p', this.buildsDir);
|
||||
shell.exec(`chown -R ${this.wwwUser} ${this.buildsDir}`);
|
||||
shell.exec(`chown -R ${this.serverUser} ${this.buildsDir}`);
|
||||
}
|
||||
|
||||
// Methods - Public
|
||||
public buildExists(pr: string, sha = '', isPublic = true, legacy = false): boolean {
|
||||
const prDir = this.getPrDir(pr, isPublic);
|
||||
const dir = !sha ? prDir : this.getShaDir(prDir, sha, legacy);
|
||||
return fs.existsSync(dir);
|
||||
}
|
||||
|
||||
public cleanUp() {
|
||||
while (this.cleanUpFns.length) {
|
||||
// Clean-up fns remove themselves from the list.
|
||||
@ -69,11 +62,11 @@ class Helper {
|
||||
}
|
||||
|
||||
public createDummyArchive(pr: string, sha: string, archivePath: string): CleanUpFn {
|
||||
const inputDir = this.getShaDir(this.getPrDir(`uploaded/${pr}`, true), sha);
|
||||
const inputDir = path.join(this.buildsDir, 'uploaded', pr, sha);
|
||||
const cmd1 = `tar --create --gzip --directory "${inputDir}" --file "${archivePath}" .`;
|
||||
const cmd2 = `chown ${this.wwwUser} ${archivePath}`;
|
||||
const cmd2 = `chown ${this.serverUser} ${archivePath}`;
|
||||
|
||||
const cleanUpTemp = this.createDummyBuild(`uploaded/${pr}`, sha, true, true);
|
||||
const cleanUpTemp = this.createDummyBuild(`uploaded/${pr}`, sha, true);
|
||||
shell.exec(cmd1);
|
||||
shell.exec(cmd2);
|
||||
cleanUpTemp();
|
||||
@ -81,44 +74,31 @@ class Helper {
|
||||
return this.createCleanUpFn(() => shell.rm('-rf', archivePath));
|
||||
}
|
||||
|
||||
public createDummyBuild(pr: string, sha: string, isPublic = true, force = false, legacy = false): CleanUpFn {
|
||||
const prDir = this.getPrDir(pr, isPublic);
|
||||
const shaDir = this.getShaDir(prDir, sha, legacy);
|
||||
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.wwwUser} ${prDir}`);
|
||||
shell.exec(`chown -R ${this.serverUser} ${prDir}`);
|
||||
|
||||
return this.createCleanUpFn(() => shell.rm('-rf', prDir));
|
||||
}
|
||||
|
||||
public deletePrDir(pr: string, isPublic = true) {
|
||||
const prDir = this.getPrDir(pr, isPublic);
|
||||
public deletePrDir(pr: string) {
|
||||
const prDir = path.join(this.buildsDir, pr);
|
||||
|
||||
if (fs.existsSync(prDir)) {
|
||||
shell.chmod('-R', 'a+w', prDir);
|
||||
// Undocumented signature (see https://github.com/shelljs/shelljs/pull/663).
|
||||
(shell as any).chmod('-R', 'a+w', prDir);
|
||||
shell.rm('-rf', prDir);
|
||||
}
|
||||
}
|
||||
|
||||
public getPrDir(pr: string, isPublic: boolean): string {
|
||||
const prDirName = isPublic ? pr : HIDDEN_DIR_PREFIX + pr;
|
||||
return path.join(this.buildsDir, prDirName);
|
||||
}
|
||||
|
||||
public getShaDir(prDir: string, sha: string, legacy = false): string {
|
||||
return path.join(prDir, legacy ? sha : this.getShordSha(sha));
|
||||
}
|
||||
|
||||
public getShordSha(sha: string): string {
|
||||
return sha.substr(0, SHORT_SHA_LEN);
|
||||
}
|
||||
|
||||
public readBuildFile(pr: string, sha: string, relFilePath: string, isPublic = true, legacy = false): string {
|
||||
const shaDir = this.getShaDir(this.getPrDir(pr, isPublic), sha, legacy);
|
||||
const absFilePath = path.join(shaDir, relFilePath);
|
||||
public readBuildFile(pr: string, sha: string, relFilePath: string): string {
|
||||
const absFilePath = path.join(this.buildsDir, pr, sha, relFilePath);
|
||||
return fs.readFileSync(absFilePath, 'utf8');
|
||||
}
|
||||
|
||||
@ -142,15 +122,14 @@ class Helper {
|
||||
statusText = status[1];
|
||||
} else {
|
||||
statusCode = status;
|
||||
statusText = http.STATUS_CODES[statusCode] || 'UNKNOWN_STATUS_CODE';
|
||||
statusText = http.STATUS_CODES[statusCode];
|
||||
}
|
||||
|
||||
return (result: CmdResult) => {
|
||||
const [headers, body] = result.stdout.
|
||||
split(/(?:\r?\n){2,}/).
|
||||
map(s => s.trim()).
|
||||
slice(-2); // In case of redirect, discard the previous headers.
|
||||
// Only keep the last to sections (final headers and body).
|
||||
slice(-2);
|
||||
|
||||
if (!result.success) {
|
||||
console.log('Stdout:', result.stdout);
|
||||
@ -164,10 +143,8 @@ class Helper {
|
||||
};
|
||||
}
|
||||
|
||||
public writeBuildFile(pr: string, sha: string, relFilePath: string, content: string, isPublic = true,
|
||||
legacy = false): CleanUpFn {
|
||||
const shaDir = this.getShaDir(this.getPrDir(pr, isPublic), sha, legacy);
|
||||
const absFilePath = path.join(shaDir, relFilePath);
|
||||
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);
|
||||
}
|
||||
|
||||
@ -189,13 +166,13 @@ class Helper {
|
||||
// Create a file with the specified content.
|
||||
fs.writeFileSync(filePath, content || '');
|
||||
}
|
||||
shell.exec(`chown ${this.wwwUser} ${filePath}`);
|
||||
shell.exec(`chown ${this.serverUser} ${filePath}`);
|
||||
|
||||
return this.createCleanUpFn(() => shell.rm('-rf', cleanUpTarget));
|
||||
}
|
||||
|
||||
// Methods - Protected
|
||||
protected createCleanUpFn(fn: () => void): CleanUpFn {
|
||||
protected createCleanUpFn(fn: Function): CleanUpFn {
|
||||
const cleanUpFn = () => {
|
||||
const idx = this.cleanUpFns.indexOf(cleanUpFn);
|
||||
if (idx !== -1) {
|
||||
|
@ -31,184 +31,111 @@ describe(`nginx`, () => {
|
||||
});
|
||||
|
||||
|
||||
h.runForAllSupportedSchemes((scheme, port) => describe(`(on ${scheme.toUpperCase()})`, () => {
|
||||
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);
|
||||
const shortSha9 = h.getShordSha(sha9);
|
||||
const shortSha0 = h.getShordSha(sha0);
|
||||
|
||||
|
||||
describe(`pr<pr>-<sha>.${host}/*`, () => {
|
||||
|
||||
describe('(for public builds)', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
h.createDummyBuild(pr, sha9);
|
||||
h.createDummyBuild(pr, sha0);
|
||||
});
|
||||
|
||||
|
||||
it('should return /index.html', done => {
|
||||
const origin = `${scheme}://pr${pr}-${shortSha9}.${host}`;
|
||||
const bodyRegex = new RegExp(`^PR: ${pr} | SHA: ${sha9} | File: /index\\.html$`);
|
||||
|
||||
Promise.all([
|
||||
h.runCmd(`curl -iL ${origin}/index.html`).then(h.verifyResponse(200, bodyRegex)),
|
||||
h.runCmd(`curl -iL ${origin}/`).then(h.verifyResponse(200, bodyRegex)),
|
||||
h.runCmd(`curl -iL ${origin}`).then(h.verifyResponse(200, bodyRegex)),
|
||||
]).then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should return /index.html (for legacy builds)', done => {
|
||||
const origin = `${scheme}://pr${pr}-${sha9}.${host}`;
|
||||
const bodyRegex = new RegExp(`^PR: ${pr} | SHA: ${sha9} | File: /index\\.html$`);
|
||||
|
||||
h.createDummyBuild(pr, sha9, true, false, true);
|
||||
|
||||
Promise.all([
|
||||
h.runCmd(`curl -iL ${origin}/index.html`).then(h.verifyResponse(200, bodyRegex)),
|
||||
h.runCmd(`curl -iL ${origin}/`).then(h.verifyResponse(200, bodyRegex)),
|
||||
h.runCmd(`curl -iL ${origin}`).then(h.verifyResponse(200, bodyRegex)),
|
||||
]).then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should return /foo/bar.js', done => {
|
||||
const bodyRegex = new RegExp(`^PR: ${pr} | SHA: ${sha9} | File: /foo/bar\\.js$`);
|
||||
|
||||
h.runCmd(`curl -iL ${scheme}://pr${pr}-${shortSha9}.${host}/foo/bar.js`).
|
||||
then(h.verifyResponse(200, bodyRegex)).
|
||||
then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should return /foo/bar.js (for legacy builds)', done => {
|
||||
const origin = `${scheme}://pr${pr}-${sha9}.${host}`;
|
||||
const bodyRegex = new RegExp(`^PR: ${pr} | SHA: ${sha9} | File: /foo/bar\\.js$`);
|
||||
|
||||
h.createDummyBuild(pr, sha9, true, false, true);
|
||||
|
||||
h.runCmd(`curl -iL ${origin}/foo/bar.js`).
|
||||
then(h.verifyResponse(200, bodyRegex)).
|
||||
then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should respond with 403 for directories', done => {
|
||||
Promise.all([
|
||||
h.runCmd(`curl -iL ${scheme}://pr${pr}-${shortSha9}.${host}/foo/`).then(h.verifyResponse(403)),
|
||||
h.runCmd(`curl -iL ${scheme}://pr${pr}-${shortSha9}.${host}/foo`).then(h.verifyResponse(403)),
|
||||
]).then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should respond with 404 for unknown paths to files', done => {
|
||||
h.runCmd(`curl -iL ${scheme}://pr${pr}-${shortSha9}.${host}/foo/baz.css`).
|
||||
then(h.verifyResponse(404)).
|
||||
then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should rewrite to \'index.html\' for unknown paths that don\'t look like files', done => {
|
||||
const origin = `${scheme}://pr${pr}-${shortSha9}.${host}`;
|
||||
const bodyRegex = new RegExp(`^PR: ${pr} | SHA: ${sha9} | File: /index\\.html$`);
|
||||
|
||||
Promise.all([
|
||||
h.runCmd(`curl -iL ${origin}/foo/baz`).then(h.verifyResponse(200, bodyRegex)),
|
||||
h.runCmd(`curl -iL ${origin}/foo/baz/`).then(h.verifyResponse(200, bodyRegex)),
|
||||
]).then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should respond with 404 for unknown PRs/SHAs', done => {
|
||||
const otherPr = 54321;
|
||||
const otherShortSha = h.getShordSha('8'.repeat(40));
|
||||
|
||||
Promise.all([
|
||||
h.runCmd(`curl -iL ${scheme}://pr${pr}9-${shortSha9}.${host}`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`curl -iL ${scheme}://pr${otherPr}-${shortSha9}.${host}`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`curl -iL ${scheme}://pr${pr}-${shortSha9}9.${host}`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`curl -iL ${scheme}://pr${pr}-${otherShortSha}.${host}`).then(h.verifyResponse(404)),
|
||||
]).then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should respond with 404 if the subdomain format is wrong', done => {
|
||||
Promise.all([
|
||||
h.runCmd(`curl -iL ${scheme}://xpr${pr}-${shortSha9}.${host}`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`curl -iL ${scheme}://prx${pr}-${shortSha9}.${host}`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`curl -iL ${scheme}://xx${pr}-${shortSha9}.${host}`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`curl -iL ${scheme}://p${pr}-${shortSha9}.${host}`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`curl -iL ${scheme}://r${pr}-${shortSha9}.${host}`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`curl -iL ${scheme}://${pr}-${shortSha9}.${host}`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`curl -iL ${scheme}://pr${pr}${shortSha9}.${host}`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`curl -iL ${scheme}://pr${pr}_${shortSha9}.${host}`).then(h.verifyResponse(404)),
|
||||
]).then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should reject PRs with leading zeros', done => {
|
||||
h.runCmd(`curl -iL ${scheme}://pr0${pr}-${shortSha9}.${host}`).
|
||||
then(h.verifyResponse(404)).
|
||||
then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should accept SHAs with leading zeros (but not trim the zeros)', done => {
|
||||
const bodyRegex9 = new RegExp(`^PR: ${pr} | SHA: ${sha9} | File: /index\\.html$`);
|
||||
const bodyRegex0 = new RegExp(`^PR: ${pr} | SHA: ${sha0} | File: /index\\.html$`);
|
||||
|
||||
Promise.all([
|
||||
h.runCmd(`curl -iL ${scheme}://pr${pr}-0${shortSha9}.${host}`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`curl -iL ${scheme}://pr${pr}-${shortSha9}.${host}`).then(h.verifyResponse(200, bodyRegex9)),
|
||||
h.runCmd(`curl -iL ${scheme}://pr${pr}-${shortSha0}.${host}`).then(h.verifyResponse(200, bodyRegex0)),
|
||||
]).then(done);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
h.createDummyBuild(pr, sha9);
|
||||
h.createDummyBuild(pr, sha0);
|
||||
});
|
||||
|
||||
|
||||
describe('(for hidden builds)', () => {
|
||||
it('should return /index.html', done => {
|
||||
const origin = `${scheme}://pr${pr}-${sha9}.${host}`;
|
||||
const bodyRegex = new RegExp(`^PR: ${pr} | SHA: ${sha9} | File: /index\\.html$`);
|
||||
|
||||
it('should respond with 404 for any file or directory', done => {
|
||||
const origin = `${scheme}://pr${pr}-${shortSha9}.${host}`;
|
||||
const assert404 = h.verifyResponse(404);
|
||||
|
||||
h.createDummyBuild(pr, sha9, false);
|
||||
expect(h.buildExists(pr, sha9, false)).toBe(true);
|
||||
|
||||
Promise.all([
|
||||
h.runCmd(`curl -iL ${origin}/index.html`).then(assert404),
|
||||
h.runCmd(`curl -iL ${origin}/`).then(assert404),
|
||||
h.runCmd(`curl -iL ${origin}`).then(assert404),
|
||||
h.runCmd(`curl -iL ${origin}/foo/bar.js`).then(assert404),
|
||||
h.runCmd(`curl -iL ${origin}/foo/`).then(assert404),
|
||||
h.runCmd(`curl -iL ${origin}/foo`).then(assert404),
|
||||
]).then(done);
|
||||
});
|
||||
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 respond with 404 for any file or directory (for legacy builds)', done => {
|
||||
const origin = `${scheme}://pr${pr}-${sha9}.${host}`;
|
||||
const assert404 = h.verifyResponse(404);
|
||||
it('should return /foo/bar.js', done => {
|
||||
const bodyRegex = new RegExp(`^PR: ${pr} | SHA: ${sha9} | File: /foo/bar\\.js$`);
|
||||
|
||||
h.createDummyBuild(pr, sha9, false, false, true);
|
||||
expect(h.buildExists(pr, sha9, false, true)).toBe(true);
|
||||
h.runCmd(`curl -iL ${scheme}://pr${pr}-${sha9}.${host}/foo/bar.js`).
|
||||
then(h.verifyResponse(200, bodyRegex)).
|
||||
then(done);
|
||||
});
|
||||
|
||||
Promise.all([
|
||||
h.runCmd(`curl -iL ${origin}/index.html`).then(assert404),
|
||||
h.runCmd(`curl -iL ${origin}/`).then(assert404),
|
||||
h.runCmd(`curl -iL ${origin}`).then(assert404),
|
||||
h.runCmd(`curl -iL ${origin}/foo/bar.js`).then(assert404),
|
||||
h.runCmd(`curl -iL ${origin}/foo/`).then(assert404),
|
||||
h.runCmd(`curl -iL ${origin}/foo`).then(assert404),
|
||||
]).then(done);
|
||||
});
|
||||
|
||||
it('should respond with 403 for directories', done => {
|
||||
Promise.all([
|
||||
h.runCmd(`curl -iL ${scheme}://pr${pr}-${sha9}.${host}/foo/`).then(h.verifyResponse(403)),
|
||||
h.runCmd(`curl -iL ${scheme}://pr${pr}-${sha9}.${host}/foo`).then(h.verifyResponse(403)),
|
||||
]).then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should respond with 404 for unknown paths to files', done => {
|
||||
h.runCmd(`curl -iL ${scheme}://pr${pr}-${sha9}.${host}/foo/baz.css`).
|
||||
then(h.verifyResponse(404)).
|
||||
then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should rewrite to \'index.html\' for unknown paths that don\'t look like files', done => {
|
||||
const bodyRegex = new RegExp(`^PR: ${pr} | SHA: ${sha9} | File: /index\\.html$`);
|
||||
|
||||
Promise.all([
|
||||
h.runCmd(`curl -iL ${scheme}://pr${pr}-${sha9}.${host}/foo/baz`).then(h.verifyResponse(200, bodyRegex)),
|
||||
h.runCmd(`curl -iL ${scheme}://pr${pr}-${sha9}.${host}/foo/baz/`).then(h.verifyResponse(200, bodyRegex)),
|
||||
]).then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should respond with 404 for unknown PRs/SHAs', done => {
|
||||
const otherPr = 54321;
|
||||
const otherSha = '8'.repeat(40);
|
||||
|
||||
Promise.all([
|
||||
h.runCmd(`curl -iL ${scheme}://pr${pr}9-${sha9}.${host}`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`curl -iL ${scheme}://pr${otherPr}-${sha9}.${host}`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`curl -iL ${scheme}://pr${pr}-${sha9}9.${host}`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`curl -iL ${scheme}://pr${pr}-${otherSha}.${host}`).then(h.verifyResponse(404)),
|
||||
]).then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should respond with 404 if the subdomain format is wrong', done => {
|
||||
Promise.all([
|
||||
h.runCmd(`curl -iL ${scheme}://xpr${pr}-${sha9}.${host}`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`curl -iL ${scheme}://prx${pr}-${sha9}.${host}`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`curl -iL ${scheme}://xx${pr}-${sha9}.${host}`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`curl -iL ${scheme}://p${pr}-${sha9}.${host}`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`curl -iL ${scheme}://r${pr}-${sha9}.${host}`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`curl -iL ${scheme}://${pr}-${sha9}.${host}`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`curl -iL ${scheme}://pr${pr}${sha9}.${host}`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`curl -iL ${scheme}://pr${pr}_${sha9}.${host}`).then(h.verifyResponse(404)),
|
||||
]).then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should reject PRs with leading zeros', done => {
|
||||
h.runCmd(`curl -iL ${scheme}://pr0${pr}-${sha9}.${host}`).
|
||||
then(h.verifyResponse(404)).
|
||||
then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should accept SHAs with leading zeros (but not trim the zeros)', done => {
|
||||
const bodyRegex9 = new RegExp(`^PR: ${pr} | SHA: ${sha9} | File: /index\\.html$`);
|
||||
const bodyRegex0 = new RegExp(`^PR: ${pr} | SHA: ${sha0} | File: /index\\.html$`);
|
||||
|
||||
Promise.all([
|
||||
h.runCmd(`curl -iL ${scheme}://pr${pr}-0${sha9}.${host}`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`curl -iL ${scheme}://pr${pr}-${sha9}.${host}`).then(h.verifyResponse(200, bodyRegex9)),
|
||||
h.runCmd(`curl -iL ${scheme}://pr${pr}-${sha0}.${host}`).then(h.verifyResponse(200, bodyRegex0)),
|
||||
]).then(done);
|
||||
});
|
||||
|
||||
});
|
||||
@ -317,54 +244,9 @@ describe(`nginx`, () => {
|
||||
});
|
||||
|
||||
|
||||
describe(`${host}/pr-updated`, () => {
|
||||
const url = `${scheme}://${host}/pr-updated`;
|
||||
|
||||
|
||||
it('should disallow non-POST requests', done => {
|
||||
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 pass requests through to the upload server', done => {
|
||||
const cmdPrefix = `curl -iLX POST --header "Content-Type: application/json"`;
|
||||
|
||||
const cmd1 = `${cmdPrefix} ${url}`;
|
||||
const cmd2 = `${cmdPrefix} --data '{"number":${pr}}' ${url}`;
|
||||
const cmd3 = `${cmdPrefix} --data '{"number":${pr},"action":"foo"}' ${url}`;
|
||||
|
||||
Promise.all([
|
||||
h.runCmd(cmd1).then(h.verifyResponse(400, /Missing or empty 'number' field/)),
|
||||
h.runCmd(cmd2).then(h.verifyResponse(200)),
|
||||
h.runCmd(cmd3).then(h.verifyResponse(200)),
|
||||
]).then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should respond with 404 for unknown paths', done => {
|
||||
const cmdPrefix = `curl -iLX POST ${scheme}://${host}`;
|
||||
|
||||
Promise.all([
|
||||
h.runCmd(`${cmdPrefix}/foo/pr-updated`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`${cmdPrefix}/foo-pr-updated`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`${cmdPrefix}/foonpr-updated`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`${cmdPrefix}/pr-updated/foo`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`${cmdPrefix}/pr-updated-foo`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`${cmdPrefix}/pr-updatednfoo`).then(h.verifyResponse(404)),
|
||||
]).then(done);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
describe(`${host}/*`, () => {
|
||||
|
||||
it('should respond with 404 for unknown URLs (even if the resource exists)', done => {
|
||||
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}`});
|
||||
|
@ -1,6 +1,5 @@
|
||||
// Imports
|
||||
import * as path from 'path';
|
||||
import * as c from './constants';
|
||||
import {helper as h} from './helper';
|
||||
|
||||
// Tests
|
||||
@ -13,307 +12,73 @@ h.runForAllSupportedSchemes((scheme, port) => describe(`integration (on ${scheme
|
||||
const archivePath = path.join(h.buildsDir, 'snapshot.tar.gz');
|
||||
|
||||
const getFile = (pr: string, sha: string, file: string) =>
|
||||
h.runCmd(`curl -iL ${scheme}://pr${pr}-${h.getShordSha(sha)}.${host}/${file}`);
|
||||
const uploadBuild = (pr: string, sha: string, archive: string, authHeader = 'Token FOO') => {
|
||||
const curlPost = `curl -iLX POST --header "Authorization: ${authHeader}"`;
|
||||
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}`);
|
||||
};
|
||||
const prUpdated = (pr: number, action?: string) => {
|
||||
const url = `${scheme}://${host}/pr-updated`;
|
||||
const payloadStr = JSON.stringify({number: pr, action});
|
||||
return h.runCmd(`curl -iLX POST --header "Content-Type: application/json" --data '${payloadStr}' ${url}`);
|
||||
};
|
||||
|
||||
beforeEach(() => jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000);
|
||||
afterEach(() => {
|
||||
h.deletePrDir(pr9);
|
||||
h.deletePrDir(pr9, false);
|
||||
h.cleanUp();
|
||||
});
|
||||
|
||||
|
||||
describe('for a new/non-existing PR', () => {
|
||||
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$`);
|
||||
|
||||
it('should be able to upload and serve a public build', done => {
|
||||
const regexPrefix9 = `^PR: uploaded\\/${pr9} \\| SHA: ${sha9} \\| File:`;
|
||||
const idxContentRegex9 = new RegExp(`${regexPrefix9} \\/index\\.html$`);
|
||||
const barContentRegex9 = new RegExp(`${regexPrefix9} \\/foo\\/bar\\.js$`);
|
||||
|
||||
h.createDummyArchive(pr9, sha9, archivePath);
|
||||
|
||||
uploadBuild(pr9, sha9, archivePath).
|
||||
then(() => Promise.all([
|
||||
getFile(pr9, sha9, 'index.html').then(h.verifyResponse(200, idxContentRegex9)),
|
||||
getFile(pr9, sha9, 'foo/bar.js').then(h.verifyResponse(200, barContentRegex9)),
|
||||
])).
|
||||
then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should be able to upload but not serve a hidden build', done => {
|
||||
const regexPrefix9 = `^PR: uploaded\\/${pr9} \\| SHA: ${sha9} \\| File:`;
|
||||
const idxContentRegex9 = new RegExp(`${regexPrefix9} \\/index\\.html$`);
|
||||
const barContentRegex9 = new RegExp(`${regexPrefix9} \\/foo\\/bar\\.js$`);
|
||||
|
||||
h.createDummyArchive(pr9, sha9, archivePath);
|
||||
|
||||
uploadBuild(pr9, sha9, archivePath, c.BV_verify_verifiedNotTrusted).
|
||||
then(() => Promise.all([
|
||||
getFile(pr9, sha9, 'index.html').then(h.verifyResponse(404)),
|
||||
getFile(pr9, sha9, 'foo/bar.js').then(h.verifyResponse(404)),
|
||||
])).
|
||||
then(() => {
|
||||
expect(h.buildExists(pr9, sha9)).toBe(false);
|
||||
expect(h.buildExists(pr9, sha9, false)).toBe(true);
|
||||
expect(h.readBuildFile(pr9, sha9, 'index.html', false)).toMatch(idxContentRegex9);
|
||||
expect(h.readBuildFile(pr9, sha9, 'foo/bar.js', false)).toMatch(barContentRegex9);
|
||||
}).
|
||||
then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should reject an upload if verification fails', done => {
|
||||
const errorRegex9 = new RegExp(`Error while verifying upload for PR ${pr9}: Test`);
|
||||
|
||||
h.createDummyArchive(pr9, sha9, archivePath);
|
||||
|
||||
uploadBuild(pr9, sha9, archivePath, c.BV_verify_error).
|
||||
then(h.verifyResponse(403, errorRegex9)).
|
||||
then(() => {
|
||||
expect(h.buildExists(pr9)).toBe(false);
|
||||
expect(h.buildExists(pr9, '', false)).toBe(false);
|
||||
}).
|
||||
then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should be able to notify that a PR has been updated (and do nothing)', done => {
|
||||
prUpdated(+pr9).
|
||||
then(h.verifyResponse(200)).
|
||||
then(() => {
|
||||
// The PR should still not exist.
|
||||
expect(h.buildExists(pr9, '', false)).toBe(false);
|
||||
expect(h.buildExists(pr9, '', true)).toBe(false);
|
||||
}).
|
||||
then(done);
|
||||
});
|
||||
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);
|
||||
});
|
||||
|
||||
|
||||
describe('for an existing PR', () => {
|
||||
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$`);
|
||||
|
||||
it('should be able to upload and serve a public build', 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$`);
|
||||
|
||||
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);
|
||||
|
||||
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);
|
||||
});
|
||||
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 be able to upload but not serve a hidden build', done => {
|
||||
const regexPrefix0 = `^PR: ${pr9} \\| SHA: ${sha0} \\| File:`;
|
||||
const idxContentRegex0 = new RegExp(`${regexPrefix0} \\/index\\.html$`);
|
||||
const barContentRegex0 = new RegExp(`${regexPrefix0} \\/foo\\/bar\\.js$`);
|
||||
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$`);
|
||||
|
||||
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, false);
|
||||
h.createDummyArchive(pr9, sha9, archivePath);
|
||||
|
||||
uploadBuild(pr9, sha9, archivePath, c.BV_verify_verifiedNotTrusted).
|
||||
then(() => Promise.all([
|
||||
getFile(pr9, sha0, 'index.html').then(h.verifyResponse(404)),
|
||||
getFile(pr9, sha0, 'foo/bar.js').then(h.verifyResponse(404)),
|
||||
getFile(pr9, sha9, 'index.html').then(h.verifyResponse(404)),
|
||||
getFile(pr9, sha9, 'foo/bar.js').then(h.verifyResponse(404)),
|
||||
])).
|
||||
then(() => {
|
||||
expect(h.buildExists(pr9, sha9)).toBe(false);
|
||||
expect(h.buildExists(pr9, sha9, false)).toBe(true);
|
||||
expect(h.readBuildFile(pr9, sha0, 'index.html', false)).toMatch(idxContentRegex0);
|
||||
expect(h.readBuildFile(pr9, sha0, 'foo/bar.js', false)).toMatch(barContentRegex0);
|
||||
expect(h.readBuildFile(pr9, sha9, 'index.html', false)).toMatch(idxContentRegex9);
|
||||
expect(h.readBuildFile(pr9, sha9, 'foo/bar.js', false)).toMatch(barContentRegex9);
|
||||
}).
|
||||
then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should reject an upload if verification fails', done => {
|
||||
const errorRegex9 = new RegExp(`Error while verifying upload for PR ${pr9}: Test`);
|
||||
|
||||
h.createDummyBuild(pr9, sha0);
|
||||
h.createDummyArchive(pr9, sha9, archivePath);
|
||||
|
||||
uploadBuild(pr9, sha9, archivePath, c.BV_verify_error).
|
||||
then(h.verifyResponse(403, errorRegex9)).
|
||||
then(() => {
|
||||
expect(h.buildExists(pr9)).toBe(true);
|
||||
expect(h.buildExists(pr9, sha0)).toBe(true);
|
||||
expect(h.buildExists(pr9, sha9)).toBe(false);
|
||||
}).
|
||||
then(done);
|
||||
|
||||
});
|
||||
|
||||
|
||||
it('should not be able to overwrite an existing public build', done => {
|
||||
const regexPrefix9 = `^PR: ${pr9} \\| SHA: ${sha9} \\| File:`;
|
||||
const idxContentRegex9 = new RegExp(`${regexPrefix9} \\/index\\.html$`);
|
||||
const barContentRegex9 = new RegExp(`${regexPrefix9} \\/foo\\/bar\\.js$`);
|
||||
|
||||
h.createDummyBuild(pr9, sha9);
|
||||
h.createDummyArchive(pr9, sha9, archivePath);
|
||||
|
||||
uploadBuild(pr9, sha9, archivePath).
|
||||
then(h.verifyResponse(409)).
|
||||
then(() => Promise.all([
|
||||
getFile(pr9, sha9, 'index.html').then(h.verifyResponse(200, idxContentRegex9)),
|
||||
getFile(pr9, sha9, 'foo/bar.js').then(h.verifyResponse(200, barContentRegex9)),
|
||||
])).
|
||||
then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should not be able to overwrite an existing hidden build', done => {
|
||||
const regexPrefix9 = `^PR: ${pr9} \\| SHA: ${sha9} \\| File:`;
|
||||
const idxContentRegex9 = new RegExp(`${regexPrefix9} \\/index\\.html$`);
|
||||
const barContentRegex9 = new RegExp(`${regexPrefix9} \\/foo\\/bar\\.js$`);
|
||||
|
||||
h.createDummyBuild(pr9, sha9, false);
|
||||
h.createDummyArchive(pr9, sha9, archivePath);
|
||||
|
||||
uploadBuild(pr9, sha9, archivePath, c.BV_verify_verifiedNotTrusted).
|
||||
then(h.verifyResponse(409)).
|
||||
then(() => {
|
||||
expect(h.readBuildFile(pr9, sha9, 'index.html', false)).toMatch(idxContentRegex9);
|
||||
expect(h.readBuildFile(pr9, sha9, 'foo/bar.js', false)).toMatch(barContentRegex9);
|
||||
}).
|
||||
then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should be able to request re-checking visibility (if outdated)', done => {
|
||||
const publicPr = pr9;
|
||||
const hiddenPr = String(c.BV_getPrIsTrusted_notTrusted);
|
||||
|
||||
h.createDummyBuild(publicPr, sha9, false);
|
||||
h.createDummyBuild(hiddenPr, sha9, true);
|
||||
|
||||
// PR visibilities are outdated (i.e. the opposte of what the should).
|
||||
expect(h.buildExists(publicPr, '', false)).toBe(true);
|
||||
expect(h.buildExists(publicPr, '', true)).toBe(false);
|
||||
expect(h.buildExists(hiddenPr, '', false)).toBe(false);
|
||||
expect(h.buildExists(hiddenPr, '', true)).toBe(true);
|
||||
|
||||
Promise.
|
||||
all([
|
||||
prUpdated(+publicPr).then(h.verifyResponse(200)),
|
||||
prUpdated(+hiddenPr).then(h.verifyResponse(200)),
|
||||
]).
|
||||
then(() => {
|
||||
// PR visibilities should have been updated.
|
||||
expect(h.buildExists(publicPr, '', false)).toBe(false);
|
||||
expect(h.buildExists(publicPr, '', true)).toBe(true);
|
||||
expect(h.buildExists(hiddenPr, '', false)).toBe(true);
|
||||
expect(h.buildExists(hiddenPr, '', true)).toBe(false);
|
||||
}).
|
||||
then(() => {
|
||||
h.deletePrDir(publicPr, true);
|
||||
h.deletePrDir(hiddenPr, false);
|
||||
}).
|
||||
then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should be able to request re-checking visibility (if up-to-date)', done => {
|
||||
const publicPr = pr9;
|
||||
const hiddenPr = String(c.BV_getPrIsTrusted_notTrusted);
|
||||
|
||||
h.createDummyBuild(publicPr, sha9, true);
|
||||
h.createDummyBuild(hiddenPr, sha9, false);
|
||||
|
||||
// PR visibilities are already up-to-date.
|
||||
expect(h.buildExists(publicPr, '', false)).toBe(false);
|
||||
expect(h.buildExists(publicPr, '', true)).toBe(true);
|
||||
expect(h.buildExists(hiddenPr, '', false)).toBe(true);
|
||||
expect(h.buildExists(hiddenPr, '', true)).toBe(false);
|
||||
|
||||
Promise.
|
||||
all([
|
||||
prUpdated(+publicPr).then(h.verifyResponse(200)),
|
||||
prUpdated(+hiddenPr).then(h.verifyResponse(200)),
|
||||
]).
|
||||
then(() => {
|
||||
// PR visibilities are still up-to-date.
|
||||
expect(h.buildExists(publicPr, '', false)).toBe(false);
|
||||
expect(h.buildExists(publicPr, '', true)).toBe(true);
|
||||
expect(h.buildExists(hiddenPr, '', false)).toBe(true);
|
||||
expect(h.buildExists(hiddenPr, '', true)).toBe(false);
|
||||
}).
|
||||
then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should reject a request if re-checking visibility fails', done => {
|
||||
const errorPr = String(c.BV_getPrIsTrusted_error);
|
||||
|
||||
h.createDummyBuild(errorPr, sha9, true);
|
||||
|
||||
expect(h.buildExists(errorPr, '', false)).toBe(false);
|
||||
expect(h.buildExists(errorPr, '', true)).toBe(true);
|
||||
|
||||
prUpdated(+errorPr).
|
||||
then(h.verifyResponse(500, /Test/)).
|
||||
then(() => {
|
||||
// PR visibility should not have been updated.
|
||||
expect(h.buildExists(errorPr, '', false)).toBe(false);
|
||||
expect(h.buildExists(errorPr, '', true)).toBe(true);
|
||||
}).
|
||||
then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should reject a request if updating visibility fails', done => {
|
||||
// One way to cause an error is to have both a public and a hidden directory for the same PR.
|
||||
h.createDummyBuild(pr9, sha9, false);
|
||||
h.createDummyBuild(pr9, sha9, true);
|
||||
|
||||
const hiddenPrDir = h.getPrDir(pr9, false);
|
||||
const publicPrDir = h.getPrDir(pr9, true);
|
||||
const bodyRegex = new RegExp(`Request to move '${hiddenPrDir}' to existing directory '${publicPrDir}'`);
|
||||
|
||||
expect(h.buildExists(pr9, '', false)).toBe(true);
|
||||
expect(h.buildExists(pr9, '', true)).toBe(true);
|
||||
|
||||
prUpdated(+pr9).
|
||||
then(h.verifyResponse(409, bodyRegex)).
|
||||
then(() => {
|
||||
// PR visibility should not have been updated.
|
||||
expect(h.buildExists(pr9, '', false)).toBe(true);
|
||||
expect(h.buildExists(pr9, '', true)).toBe(true);
|
||||
}).
|
||||
then(done);
|
||||
});
|
||||
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);
|
||||
});
|
||||
|
||||
}));
|
||||
|
@ -1,38 +0,0 @@
|
||||
// Imports
|
||||
import {GithubPullRequests} from '../common/github-pull-requests';
|
||||
import {BUILD_VERIFICATION_STATUS, BuildVerifier} from '../upload-server/build-verifier';
|
||||
import {UploadError} from '../upload-server/upload-error';
|
||||
import * as c from './constants';
|
||||
|
||||
// Run
|
||||
// TODO(gkalpak): Add e2e tests to cover these interactions as well.
|
||||
GithubPullRequests.prototype.addComment = () => Promise.resolve();
|
||||
BuildVerifier.prototype.getPrIsTrusted = (pr: number) => {
|
||||
switch (pr) {
|
||||
case c.BV_getPrIsTrusted_error:
|
||||
// For e2e tests, fake an error.
|
||||
return Promise.reject('Test');
|
||||
case c.BV_getPrIsTrusted_notTrusted:
|
||||
// For e2e tests, fake an untrusted PR (`false`).
|
||||
return Promise.resolve(false);
|
||||
default:
|
||||
// For e2e tests, default to trusted PRs (`true`).
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
};
|
||||
BuildVerifier.prototype.verify = (expectedPr: number, authHeader: string) => {
|
||||
switch (authHeader) {
|
||||
case c.BV_verify_error:
|
||||
// For e2e tests, fake a verification error.
|
||||
return Promise.reject(new UploadError(403, `Error while verifying upload for PR ${expectedPr}: Test`));
|
||||
case c.BV_verify_verifiedNotTrusted:
|
||||
// For e2e tests, fake a `verifiedNotTrusted` verification status.
|
||||
return Promise.resolve(BUILD_VERIFICATION_STATUS.verifiedNotTrusted);
|
||||
default:
|
||||
// For e2e tests, default to `verifiedAndTrusted` verification status.
|
||||
return Promise.resolve(BUILD_VERIFICATION_STATUS.verifiedAndTrusted);
|
||||
}
|
||||
};
|
||||
|
||||
// tslint:disable-next-line: no-var-requires
|
||||
require('../upload-server/index');
|
@ -1,7 +1,6 @@
|
||||
// Imports
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as c from './constants';
|
||||
import {CmdResult, helper as h} from './helper';
|
||||
|
||||
// Tests
|
||||
@ -20,19 +19,18 @@ describe('upload-server (on HTTP)', () => {
|
||||
describe(`${host}/create-build/<pr>/<sha>`, () => {
|
||||
const authorizationHeader = `--header "Authorization: Token FOO"`;
|
||||
const xFileHeader = `--header "X-File: ${h.buildsDir}/snapshot.tar.gz"`;
|
||||
const defaultHeaders = `${authorizationHeader} ${xFileHeader}`;
|
||||
const curl = (url: string, headers = defaultHeaders) => `curl -iL ${headers} ${url}`;
|
||||
const curl = `curl -iL ${authorizationHeader} ${xFileHeader}`;
|
||||
|
||||
|
||||
it('should disallow non-GET requests', done => {
|
||||
const url = `http://${host}/create-build/${pr}/${sha9}`;
|
||||
const bodyRegex = /^Unknown resource/;
|
||||
const bodyRegex = /^Unsupported method/;
|
||||
|
||||
Promise.all([
|
||||
h.runCmd(`curl -iLX PUT ${url}`).then(h.verifyResponse(404, bodyRegex)),
|
||||
h.runCmd(`curl -iLX POST ${url}`).then(h.verifyResponse(404, bodyRegex)),
|
||||
h.runCmd(`curl -iLX PATCH ${url}`).then(h.verifyResponse(404, bodyRegex)),
|
||||
h.runCmd(`curl -iLX DELETE ${url}`).then(h.verifyResponse(404, bodyRegex)),
|
||||
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);
|
||||
});
|
||||
|
||||
@ -44,8 +42,8 @@ describe('upload-server (on HTTP)', () => {
|
||||
const bodyRegex = /^Missing or empty 'AUTHORIZATION' header/;
|
||||
|
||||
Promise.all([
|
||||
h.runCmd(curl(url, headers1)).then(h.verifyResponse(401, bodyRegex)),
|
||||
h.runCmd(curl(url, headers2)).then(h.verifyResponse(401, bodyRegex)),
|
||||
h.runCmd(`curl -iL ${headers1} ${url}`).then(h.verifyResponse(401, bodyRegex)),
|
||||
h.runCmd(`curl -iL ${headers2} ${url}`).then(h.verifyResponse(401, bodyRegex)),
|
||||
]).then(done);
|
||||
});
|
||||
|
||||
@ -57,25 +55,14 @@ describe('upload-server (on HTTP)', () => {
|
||||
const bodyRegex = /^Missing or empty 'X-FILE' header/;
|
||||
|
||||
Promise.all([
|
||||
h.runCmd(curl(url, headers1)).then(h.verifyResponse(400, bodyRegex)),
|
||||
h.runCmd(curl(url, headers2)).then(h.verifyResponse(400, bodyRegex)),
|
||||
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 reject requests for which the PR verification fails', done => {
|
||||
const headers = `--header "Authorization: ${c.BV_verify_error}" ${xFileHeader}`;
|
||||
const url = `http://${host}/create-build/${pr}/${sha9}`;
|
||||
const bodyRegex = new RegExp(`Error while verifying upload for PR ${pr}: Test`);
|
||||
|
||||
h.runCmd(curl(url, headers)).
|
||||
then(h.verifyResponse(403, bodyRegex)).
|
||||
then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should respond with 404 for unknown paths', done => {
|
||||
const cmdPrefix = curl(`http://${host}`);
|
||||
const cmdPrefix = `${curl} http://${host}`;
|
||||
|
||||
Promise.all([
|
||||
h.runCmd(`${cmdPrefix}/foo/create-build/${pr}/${sha9}`).then(h.verifyResponse(404)),
|
||||
@ -91,7 +78,7 @@ describe('upload-server (on HTTP)', () => {
|
||||
|
||||
|
||||
it('should reject PRs with leading zeros', done => {
|
||||
h.runCmd(curl(`http://${host}/create-build/0${pr}/${sha9}`)).
|
||||
h.runCmd(`${curl} http://${host}/create-build/0${pr}/${sha9}`).
|
||||
then(h.verifyResponse(404)).
|
||||
then(done);
|
||||
});
|
||||
@ -99,254 +86,129 @@ describe('upload-server (on HTTP)', () => {
|
||||
|
||||
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)),
|
||||
h.runCmd(`${curl} http://${host}/create-build/${pr}/0${sha9}`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`${curl} http://${host}/create-build/${pr}/${sha9}`).then(h.verifyResponse(500)),
|
||||
h.runCmd(`${curl} http://${host}/create-build/${pr}/${sha0}`).then(h.verifyResponse(500)),
|
||||
]).then(done);
|
||||
});
|
||||
|
||||
|
||||
[true, false].forEach(isPublic => describe(`(for ${isPublic ? 'public' : 'hidden'} builds)`, () => {
|
||||
const authorizationHeader2 = isPublic ?
|
||||
authorizationHeader : `--header "Authorization: ${c.BV_verify_verifiedNotTrusted}"`;
|
||||
const cmdPrefix = curl('', `${authorizationHeader2} ${xFileHeader}`);
|
||||
const overwriteRe = RegExp(`^Request to overwrite existing ${isPublic ? 'public' : 'non-public'} directory`);
|
||||
it('should not overwrite existing builds', done => {
|
||||
h.createDummyBuild(pr, sha9);
|
||||
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 not overwrite existing builds', done => {
|
||||
h.createDummyBuild(pr, sha9, isPublic);
|
||||
expect(h.readBuildFile(pr, sha9, 'index.html', isPublic)).toContain('index.html');
|
||||
it('should delete the PR directory on error (for new PR)', done => {
|
||||
const prDir = path.join(h.buildsDir, pr);
|
||||
|
||||
h.writeBuildFile(pr, sha9, 'index.html', 'My content', isPublic);
|
||||
expect(h.readBuildFile(pr, sha9, 'index.html', isPublic)).toBe('My content');
|
||||
h.runCmd(`${curl} http://${host}/create-build/${pr}/${sha9}`).
|
||||
then(h.verifyResponse(500)).
|
||||
then(() => expect(fs.existsSync(prDir)).toBe(false)).
|
||||
then(done);
|
||||
});
|
||||
|
||||
h.runCmd(`${cmdPrefix} http://${host}/create-build/${pr}/${sha9}`).
|
||||
then(h.verifyResponse(409, overwriteRe)).
|
||||
then(() => expect(h.readBuildFile(pr, sha9, 'index.html', isPublic)).toBe('My content')).
|
||||
then(done);
|
||||
|
||||
it('should 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 not overwrite existing builds (even if the SHA is different)', done => {
|
||||
// Since only the first few characters of the SHA are used, it is possible for two different
|
||||
// SHAs to correspond to the same directory. In that case, we don't want the second SHA to
|
||||
// overwrite the first.
|
||||
|
||||
const sha9Almost = sha9.replace(/.$/, '8');
|
||||
expect(sha9Almost).not.toBe(sha9);
|
||||
|
||||
h.createDummyBuild(pr, sha9, isPublic);
|
||||
expect(h.readBuildFile(pr, sha9, 'index.html', isPublic)).toContain('index.html');
|
||||
|
||||
h.writeBuildFile(pr, sha9, 'index.html', 'My content', isPublic);
|
||||
expect(h.readBuildFile(pr, sha9, 'index.html', isPublic)).toBe('My content');
|
||||
|
||||
h.runCmd(`${cmdPrefix} http://${host}/create-build/${pr}/${sha9Almost}`).
|
||||
then(h.verifyResponse(409, overwriteRe)).
|
||||
then(() => expect(h.readBuildFile(pr, sha9, 'index.html', isPublic)).toBe('My content')).
|
||||
then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should delete the PR directory on error (for new PR)', done => {
|
||||
h.runCmd(`${cmdPrefix} http://${host}/create-build/${pr}/${sha9}`).
|
||||
then(h.verifyResponse(500)).
|
||||
then(() => expect(h.buildExists(pr, '', isPublic)).toBe(false)).
|
||||
then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should only delete the SHA directory on error (for existing PR)', done => {
|
||||
h.createDummyBuild(pr, sha0, isPublic);
|
||||
|
||||
h.runCmd(`${cmdPrefix} http://${host}/create-build/${pr}/${sha9}`).
|
||||
then(h.verifyResponse(500)).
|
||||
it('should extract the contents of the uploaded file', done => {
|
||||
uploadPromise.
|
||||
then(() => {
|
||||
expect(h.buildExists(pr, sha9, isPublic)).toBe(false);
|
||||
expect(h.buildExists(pr, '', isPublic)).toBe(true);
|
||||
expect(h.readBuildFile(pr, sha9, 'index.html')).toContain(`uploaded/${pr}`);
|
||||
expect(h.readBuildFile(pr, sha9, 'foo/bar.js')).toContain(`uploaded/${pr}`);
|
||||
}).
|
||||
then(done);
|
||||
});
|
||||
|
||||
|
||||
describe('on successful upload', () => {
|
||||
const archivePath = path.join(h.buildsDir, 'snapshot.tar.gz');
|
||||
const statusCode = isPublic ? 201 : 202;
|
||||
let uploadPromise: Promise<CmdResult>;
|
||||
|
||||
beforeEach(() => {
|
||||
h.createDummyArchive(pr, sha9, archivePath);
|
||||
uploadPromise = h.runCmd(`${cmdPrefix} http://${host}/create-build/${pr}/${sha9}`);
|
||||
});
|
||||
afterEach(() => h.deletePrDir(pr, isPublic));
|
||||
|
||||
|
||||
it(`should respond with ${statusCode}`, done => {
|
||||
uploadPromise.then(h.verifyResponse(statusCode)).then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should extract the contents of the uploaded file', done => {
|
||||
uploadPromise.
|
||||
then(() => {
|
||||
expect(h.readBuildFile(pr, sha9, 'index.html', isPublic)).toContain(`uploaded/${pr}`);
|
||||
expect(h.readBuildFile(pr, sha9, 'foo/bar.js', isPublic)).toContain(`uploaded/${pr}`);
|
||||
}).
|
||||
then(done);
|
||||
});
|
||||
|
||||
|
||||
it(`should create files/directories owned by '${h.wwwUser}'`, done => {
|
||||
const prDir = h.getPrDir(pr, isPublic);
|
||||
const shaDir = h.getShaDir(prDir, sha9);
|
||||
const idxPath = path.join(shaDir, 'index.html');
|
||||
const barPath = path.join(shaDir, 'foo', 'bar.js');
|
||||
|
||||
uploadPromise.
|
||||
then(() => Promise.all([
|
||||
h.runCmd(`find ${shaDir}`),
|
||||
h.runCmd(`find ${shaDir} -user ${h.wwwUser}`),
|
||||
])).
|
||||
then(([{stdout: allFiles}, {stdout: userFiles}]) => {
|
||||
expect(userFiles).toBe(allFiles);
|
||||
expect(userFiles).toContain(shaDir);
|
||||
expect(userFiles).toContain(idxPath);
|
||||
expect(userFiles).toContain(barPath);
|
||||
}).
|
||||
then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should delete the uploaded file', done => {
|
||||
expect(fs.existsSync(archivePath)).toBe(true);
|
||||
uploadPromise.
|
||||
then(() => expect(fs.existsSync(archivePath)).toBe(false)).
|
||||
then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should make the build directory non-writable', done => {
|
||||
const prDir = h.getPrDir(pr, isPublic);
|
||||
const shaDir = h.getShaDir(prDir, sha9);
|
||||
const idxPath = path.join(shaDir, 'index.html');
|
||||
const barPath = path.join(shaDir, 'foo', 'bar.js');
|
||||
|
||||
// See https://github.com/nodejs/node-v0.x-archive/issues/3045#issuecomment-4862588.
|
||||
const isNotWritable = (fileOrDir: string) => {
|
||||
const mode = fs.statSync(fileOrDir).mode;
|
||||
// tslint:disable-next-line: no-bitwise
|
||||
return !(mode & parseInt('222', 8));
|
||||
};
|
||||
|
||||
uploadPromise.
|
||||
then(() => {
|
||||
expect(isNotWritable(shaDir)).toBe(true);
|
||||
expect(isNotWritable(idxPath)).toBe(true);
|
||||
expect(isNotWritable(barPath)).toBe(true);
|
||||
}).
|
||||
then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should ignore a legacy 40-chars long build directory (even if it starts with the same chars)', done => {
|
||||
// It is possible that 40-chars long build directories exist, if they had been deployed
|
||||
// before implementing the shorter build directory names. In that case, we don't want the
|
||||
// second (shorter) name to be considered the same as the old one (even if they originate
|
||||
// from the same SHA).
|
||||
|
||||
h.createDummyBuild(pr, sha9, isPublic, false, true);
|
||||
expect(h.readBuildFile(pr, sha9, 'index.html', isPublic, true)).toContain('index.html');
|
||||
|
||||
h.writeBuildFile(pr, sha9, 'index.html', 'My content', isPublic, true);
|
||||
expect(h.readBuildFile(pr, sha9, 'index.html', isPublic, true)).toBe('My content');
|
||||
|
||||
h.runCmd(`${cmdPrefix} http://${host}/create-build/${pr}/${sha9}`).
|
||||
then(h.verifyResponse(statusCode)).
|
||||
then(() => {
|
||||
expect(h.buildExists(pr, sha9, isPublic)).toBe(true);
|
||||
expect(h.buildExists(pr, sha9, isPublic, true)).toBe(true);
|
||||
expect(h.readBuildFile(pr, sha9, 'index.html', isPublic)).toContain('index.html');
|
||||
expect(h.readBuildFile(pr, sha9, 'index.html', isPublic, true)).toBe('My content');
|
||||
}).
|
||||
then(done);
|
||||
});
|
||||
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);
|
||||
});
|
||||
|
||||
|
||||
describe('when the PR\'s visibility has changed', () => {
|
||||
const archivePath = path.join(h.buildsDir, 'snapshot.tar.gz');
|
||||
const statusCode = isPublic ? 201 : 202;
|
||||
it('should delete the uploaded file', done => {
|
||||
expect(fs.existsSync(archivePath)).toBe(true);
|
||||
uploadPromise.
|
||||
then(() => expect(fs.existsSync(archivePath)).toBe(false)).
|
||||
then(done);
|
||||
});
|
||||
|
||||
const checkPrVisibility = (isPublic2: boolean) => {
|
||||
expect(h.buildExists(pr, '', isPublic2)).toBe(true);
|
||||
expect(h.buildExists(pr, '', !isPublic2)).toBe(false);
|
||||
expect(h.buildExists(pr, sha0, isPublic2)).toBe(true);
|
||||
expect(h.buildExists(pr, sha0, !isPublic2)).toBe(false);
|
||||
|
||||
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));
|
||||
};
|
||||
const uploadBuild = (sha: string) => h.runCmd(`${cmdPrefix} http://${host}/create-build/${pr}/${sha}`);
|
||||
|
||||
beforeEach(() => {
|
||||
h.createDummyBuild(pr, sha0, !isPublic);
|
||||
h.createDummyArchive(pr, sha9, archivePath);
|
||||
checkPrVisibility(!isPublic);
|
||||
});
|
||||
afterEach(() => h.deletePrDir(pr, isPublic));
|
||||
|
||||
|
||||
it('should update the PR\'s visibility', done => {
|
||||
uploadBuild(sha9).
|
||||
then(h.verifyResponse(statusCode)).
|
||||
then(() => {
|
||||
checkPrVisibility(isPublic);
|
||||
expect(h.buildExists(pr, sha9, isPublic)).toBe(true);
|
||||
expect(h.readBuildFile(pr, sha9, 'index.html', isPublic)).toContain(`uploaded/${pr}`);
|
||||
expect(h.readBuildFile(pr, sha9, 'index.html', isPublic)).toContain(sha9);
|
||||
}).
|
||||
then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should not overwrite existing builds (but keep the updated visibility)', done => {
|
||||
expect(h.buildExists(pr, sha0, isPublic)).toBe(false);
|
||||
|
||||
uploadBuild(sha0).
|
||||
then(h.verifyResponse(409, overwriteRe)).
|
||||
then(() => {
|
||||
checkPrVisibility(isPublic);
|
||||
expect(h.readBuildFile(pr, sha0, 'index.html', isPublic)).toContain(pr);
|
||||
expect(h.readBuildFile(pr, sha0, 'index.html', isPublic)).not.toContain(`uploaded/${pr}`);
|
||||
expect(h.readBuildFile(pr, sha0, 'index.html', isPublic)).toContain(sha0);
|
||||
expect(h.readBuildFile(pr, sha0, 'index.html', isPublic)).not.toContain(sha9);
|
||||
}).
|
||||
then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should reject the request if it fails to update the PR\'s visibility', done => {
|
||||
// One way to cause an error is to have both a public and a hidden directory for the same PR.
|
||||
h.createDummyBuild(pr, sha0, isPublic);
|
||||
|
||||
expect(h.buildExists(pr, sha0, isPublic)).toBe(true);
|
||||
expect(h.buildExists(pr, sha0, !isPublic)).toBe(true);
|
||||
|
||||
const errorRegex = new RegExp(`^Request to move '${h.getPrDir(pr, !isPublic)}' ` +
|
||||
`to existing directory '${h.getPrDir(pr, isPublic)}'.`);
|
||||
|
||||
uploadBuild(sha9).
|
||||
then(h.verifyResponse(409, errorRegex)).
|
||||
then(() => {
|
||||
expect(h.buildExists(pr, sha0, isPublic)).toBe(true);
|
||||
expect(h.buildExists(pr, sha0, !isPublic)).toBe(true);
|
||||
expect(h.buildExists(pr, sha9, isPublic)).toBe(false);
|
||||
expect(h.buildExists(pr, sha9, !isPublic)).toBe(false);
|
||||
}).
|
||||
then(done);
|
||||
});
|
||||
|
||||
uploadPromise.
|
||||
then(() => {
|
||||
expect(isNotWritable(shaDir)).toBe(true);
|
||||
expect(isNotWritable(idxPath)).toBe(true);
|
||||
expect(isNotWritable(barPath)).toBe(true);
|
||||
}).
|
||||
then(done);
|
||||
});
|
||||
|
||||
}));
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@ -375,194 +237,27 @@ describe('upload-server (on HTTP)', () => {
|
||||
});
|
||||
|
||||
|
||||
describe(`${host}/pr-updated`, () => {
|
||||
const url = `http://${host}/pr-updated`;
|
||||
|
||||
// Helpers
|
||||
const curl = (payload?: {number: number, action?: string}) => {
|
||||
const payloadStr = payload && JSON.stringify(payload) || '';
|
||||
return `curl -iLX POST --header "Content-Type: application/json" --data '${payloadStr}' ${url}`;
|
||||
};
|
||||
|
||||
|
||||
it('should disallow non-POST requests', done => {
|
||||
const bodyRegex = /^Unknown resource in request/;
|
||||
|
||||
Promise.all([
|
||||
h.runCmd(`curl -iLX GET ${url}`).then(h.verifyResponse(404, bodyRegex)),
|
||||
h.runCmd(`curl -iLX PUT ${url}`).then(h.verifyResponse(404, bodyRegex)),
|
||||
h.runCmd(`curl -iLX PATCH ${url}`).then(h.verifyResponse(404, bodyRegex)),
|
||||
h.runCmd(`curl -iLX DELETE ${url}`).then(h.verifyResponse(404, bodyRegex)),
|
||||
]).then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should respond with 400 for requests without a payload', done => {
|
||||
const bodyRegex = /^Missing or empty 'number' field in request/;
|
||||
|
||||
h.runCmd(curl()).
|
||||
then(h.verifyResponse(400, bodyRegex)).
|
||||
then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should respond with 400 for requests without a \'number\' field', done => {
|
||||
const bodyRegex = /^Missing or empty 'number' field in request/;
|
||||
|
||||
Promise.all([
|
||||
h.runCmd(curl({} as any)).then(h.verifyResponse(400, bodyRegex)),
|
||||
h.runCmd(curl({number: null} as any)).then(h.verifyResponse(400, bodyRegex)),
|
||||
]).then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should reject requests for which checking the PR visibility fails', done => {
|
||||
h.runCmd(curl({number: c.BV_getPrIsTrusted_error})).
|
||||
then(h.verifyResponse(500, /Test/)).
|
||||
then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should respond with 404 for unknown paths', done => {
|
||||
const mockPayload = JSON.stringify({number: +pr});
|
||||
const cmdPrefix = `curl -iLX POST --data "${mockPayload}" http://${host}`;
|
||||
|
||||
Promise.all([
|
||||
h.runCmd(`${cmdPrefix}/foo/pr-updated`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`${cmdPrefix}/foo-pr-updated`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`${cmdPrefix}/foonpr-updated`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`${cmdPrefix}/pr-updated/foo`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`${cmdPrefix}/pr-updated-foo`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`${cmdPrefix}/pr-updatednfoo`).then(h.verifyResponse(404)),
|
||||
]).then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should do nothing if PR\'s visibility is already up-to-date', done => {
|
||||
const publicPr = pr;
|
||||
const hiddenPr = String(c.BV_getPrIsTrusted_notTrusted);
|
||||
const checkVisibilities = () => {
|
||||
// Public build is already public.
|
||||
expect(h.buildExists(publicPr, '', false)).toBe(false);
|
||||
expect(h.buildExists(publicPr, '', true)).toBe(true);
|
||||
// Hidden build is already hidden.
|
||||
expect(h.buildExists(hiddenPr, '', false)).toBe(true);
|
||||
expect(h.buildExists(hiddenPr, '', true)).toBe(false);
|
||||
};
|
||||
|
||||
h.createDummyBuild(publicPr, sha9, true);
|
||||
h.createDummyBuild(hiddenPr, sha9, false);
|
||||
checkVisibilities();
|
||||
|
||||
Promise.
|
||||
all([
|
||||
h.runCmd(curl({number: +publicPr, action: 'foo'})).then(h.verifyResponse(200)),
|
||||
h.runCmd(curl({number: +hiddenPr, action: 'foo'})).then(h.verifyResponse(200)),
|
||||
]).
|
||||
// Visibilities should not have changed, because the specified action could not have triggered a change.
|
||||
then(checkVisibilities).
|
||||
then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should do nothing if \'action\' implies no visibility change', done => {
|
||||
const publicPr = pr;
|
||||
const hiddenPr = String(c.BV_getPrIsTrusted_notTrusted);
|
||||
const checkVisibilities = () => {
|
||||
// Public build is hidden atm.
|
||||
expect(h.buildExists(publicPr, '', false)).toBe(true);
|
||||
expect(h.buildExists(publicPr, '', true)).toBe(false);
|
||||
// Hidden build is public atm.
|
||||
expect(h.buildExists(hiddenPr, '', false)).toBe(false);
|
||||
expect(h.buildExists(hiddenPr, '', true)).toBe(true);
|
||||
};
|
||||
|
||||
h.createDummyBuild(publicPr, sha9, false);
|
||||
h.createDummyBuild(hiddenPr, sha9, true);
|
||||
checkVisibilities();
|
||||
|
||||
Promise.
|
||||
all([
|
||||
h.runCmd(curl({number: +publicPr, action: 'foo'})).then(h.verifyResponse(200)),
|
||||
h.runCmd(curl({number: +hiddenPr, action: 'foo'})).then(h.verifyResponse(200)),
|
||||
]).
|
||||
// Visibilities should not have changed, because the specified action could not have triggered a change.
|
||||
then(checkVisibilities).
|
||||
then(done);
|
||||
});
|
||||
|
||||
|
||||
describe('when the visiblity has changed', () => {
|
||||
const publicPr = pr;
|
||||
const hiddenPr = String(c.BV_getPrIsTrusted_notTrusted);
|
||||
|
||||
beforeEach(() => {
|
||||
// Create initial PR builds with opposite visibilities as the ones that will be reported:
|
||||
// - The now public PR was previously hidden.
|
||||
// - The now hidden PR was previously public.
|
||||
h.createDummyBuild(publicPr, sha9, false);
|
||||
h.createDummyBuild(hiddenPr, sha9, true);
|
||||
|
||||
expect(h.buildExists(publicPr, '', false)).toBe(true);
|
||||
expect(h.buildExists(publicPr, '', true)).toBe(false);
|
||||
expect(h.buildExists(hiddenPr, '', false)).toBe(false);
|
||||
expect(h.buildExists(hiddenPr, '', true)).toBe(true);
|
||||
});
|
||||
afterEach(() => {
|
||||
// Expect PRs' visibility to have been updated:
|
||||
// - The public PR should be actually public (previously it was hidden).
|
||||
// - The hidden PR should be actually hidden (previously it was public).
|
||||
expect(h.buildExists(publicPr, '', false)).toBe(false);
|
||||
expect(h.buildExists(publicPr, '', true)).toBe(true);
|
||||
expect(h.buildExists(hiddenPr, '', false)).toBe(true);
|
||||
expect(h.buildExists(hiddenPr, '', true)).toBe(false);
|
||||
|
||||
h.deletePrDir(publicPr, true);
|
||||
h.deletePrDir(hiddenPr, false);
|
||||
});
|
||||
|
||||
|
||||
it('should update the PR\'s visibility (action: undefined)', done => {
|
||||
Promise.all([
|
||||
h.runCmd(curl({number: +publicPr})).then(h.verifyResponse(200)),
|
||||
h.runCmd(curl({number: +hiddenPr})).then(h.verifyResponse(200)),
|
||||
]).then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should update the PR\'s visibility (action: labeled)', done => {
|
||||
Promise.all([
|
||||
h.runCmd(curl({number: +publicPr, action: 'labeled'})).then(h.verifyResponse(200)),
|
||||
h.runCmd(curl({number: +hiddenPr, action: 'labeled'})).then(h.verifyResponse(200)),
|
||||
]).then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should update the PR\'s visibility (action: unlabeled)', done => {
|
||||
Promise.all([
|
||||
h.runCmd(curl({number: +publicPr, action: 'unlabeled'})).then(h.verifyResponse(200)),
|
||||
h.runCmd(curl({number: +hiddenPr, action: 'unlabeled'})).then(h.verifyResponse(200)),
|
||||
]).then(done);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
describe(`${host}/*`, () => {
|
||||
|
||||
it('should respond with 404 for requests to unknown URLs', done => {
|
||||
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)),
|
||||
h.runCmd(`curl -iLX PUT http://${host}`).then(h.verifyResponse(404, bodyRegex)),
|
||||
h.runCmd(`curl -iLX POST http://${host}`).then(h.verifyResponse(404, bodyRegex)),
|
||||
h.runCmd(`curl -iLX PATCH http://${host}`).then(h.verifyResponse(404, bodyRegex)),
|
||||
h.runCmd(`curl -iLX DELETE http://${host}`).then(h.verifyResponse(404, bodyRegex)),
|
||||
]).then(done);
|
||||
});
|
||||
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
|
@ -6,40 +6,38 @@
|
||||
"author": "Angular",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"prebuild": "yarn clean-dist",
|
||||
"prebuild": "yarn run clean",
|
||||
"build": "tsc",
|
||||
"build-watch": "yarn build --watch",
|
||||
"clean-dist": "node --eval \"require('shelljs').rm('-rf', 'dist')\"",
|
||||
"dev": "concurrently --kill-others --raw --success first \"yarn build-watch\" \"yarn test-watch\"",
|
||||
"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 lint",
|
||||
"pre~~test-only": "yarn run lint",
|
||||
"~~test-only": "node dist/test",
|
||||
"pretest": "yarn build",
|
||||
"test": "yarn ~~test-only",
|
||||
"pretest-watch": "yarn build",
|
||||
"test-watch": "nodemon --exec \"yarn ~~test-only\" --watch dist"
|
||||
"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": {
|
||||
"body-parser": "^1.18.2",
|
||||
"express": "^4.15.4",
|
||||
"jasmine": "^2.8.0",
|
||||
"jsonwebtoken": "^8.0.1",
|
||||
"shelljs": "^0.7.8",
|
||||
"tslib": "^1.7.1"
|
||||
"express": "^4.14.1",
|
||||
"jasmine": "^2.5.3",
|
||||
"jsonwebtoken": "^7.3.0",
|
||||
"shelljs": "^0.7.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/body-parser": "^1.16.5",
|
||||
"@types/express": "^4.0.37",
|
||||
"@types/jasmine": "^2.6.0",
|
||||
"@types/jsonwebtoken": "^7.2.3",
|
||||
"@types/node": "^8.0.30",
|
||||
"@types/shelljs": "^0.8.0",
|
||||
"@types/supertest": "^2.0.3",
|
||||
"concurrently": "^3.5.0",
|
||||
"nodemon": "^1.12.1",
|
||||
"@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": "^5.7.0",
|
||||
"tslint-jasmine-noSkipOrFocus": "^1.0.8",
|
||||
"typescript": "^2.5.2"
|
||||
"tslint": "^4.4.2",
|
||||
"typescript": "^2.1.6"
|
||||
}
|
||||
}
|
||||
|
@ -1,9 +1,7 @@
|
||||
// Imports
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as shell from 'shelljs';
|
||||
import {BuildCleaner} from '../../lib/clean-up/build-cleaner';
|
||||
import {HIDDEN_DIR_PREFIX} from '../../lib/common/constants';
|
||||
import {GithubPullRequests} from '../../lib/common/github-pull-requests';
|
||||
|
||||
// Tests
|
||||
@ -39,8 +37,8 @@ describe('BuildCleaner', () => {
|
||||
let cleanerGetExistingBuildNumbersSpy: jasmine.Spy;
|
||||
let cleanerGetOpenPrNumbersSpy: jasmine.Spy;
|
||||
let cleanerRemoveUnnecessaryBuildsSpy: jasmine.Spy;
|
||||
let existingBuildsDeferred: {resolve: (v?: any) => void, reject: (e?: any) => void};
|
||||
let openPrsDeferred: {resolve: (v?: any) => void, reject: (e?: any) => void};
|
||||
let existingBuildsDeferred: {resolve: Function, reject: Function};
|
||||
let openPrsDeferred: {resolve: Function, reject: Function};
|
||||
let promise: Promise<void>;
|
||||
|
||||
beforeEach(() => {
|
||||
@ -116,7 +114,7 @@ describe('BuildCleaner', () => {
|
||||
|
||||
it('should resolve with the value returned by \'removeUnnecessaryBuilds()\'', done => {
|
||||
promise.then(result => {
|
||||
expect(result as any).toBe('Test');
|
||||
expect(result).toBe('Test');
|
||||
done();
|
||||
});
|
||||
|
||||
@ -172,16 +170,6 @@ describe('BuildCleaner', () => {
|
||||
});
|
||||
|
||||
|
||||
it('should remove `HIDDEN_DIR_PREFIX` from the filenames', done => {
|
||||
promise.then(result => {
|
||||
expect(result).toEqual([12, 34, 56]);
|
||||
done();
|
||||
});
|
||||
|
||||
readdirCb(null, [`${HIDDEN_DIR_PREFIX}12`, '34', `${HIDDEN_DIR_PREFIX}56`]);
|
||||
});
|
||||
|
||||
|
||||
it('should ignore files with non-numeric (or zero) names', done => {
|
||||
promise.then(result => {
|
||||
expect(result).toEqual([12, 34, 56]);
|
||||
@ -195,7 +183,7 @@ describe('BuildCleaner', () => {
|
||||
|
||||
|
||||
describe('getOpenPrNumbers()', () => {
|
||||
let prDeferred: {resolve: (v: any) => void, reject: (v: any) => void};
|
||||
let prDeferred: {resolve: Function, reject: Function};
|
||||
let promise: Promise<number[]>;
|
||||
|
||||
beforeEach(() => {
|
||||
@ -242,22 +230,10 @@ describe('BuildCleaner', () => {
|
||||
describe('removeDir()', () => {
|
||||
let shellChmodSpy: jasmine.Spy;
|
||||
let shellRmSpy: jasmine.Spy;
|
||||
let shellTestSpy: jasmine.Spy;
|
||||
|
||||
beforeEach(() => {
|
||||
shellChmodSpy = spyOn(shell, 'chmod');
|
||||
shellRmSpy = spyOn(shell, 'rm');
|
||||
shellTestSpy = spyOn(shell, 'test').and.returnValue(true);
|
||||
});
|
||||
|
||||
|
||||
it('should test if the directory exists (and return if is does not)', () => {
|
||||
shellTestSpy.and.returnValue(false);
|
||||
(cleaner as any).removeDir('/foo/bar');
|
||||
|
||||
expect(shellTestSpy).toHaveBeenCalledWith('-d', '/foo/bar');
|
||||
expect(shellChmodSpy).not.toHaveBeenCalled();
|
||||
expect(shellRmSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
|
||||
@ -277,10 +253,7 @@ describe('BuildCleaner', () => {
|
||||
|
||||
it('should catch errors and log them', () => {
|
||||
const consoleErrorSpy = spyOn(console, 'error');
|
||||
shellRmSpy.and.callFake(() => {
|
||||
// tslint:disable-next-line: no-string-throw
|
||||
throw 'Test';
|
||||
});
|
||||
shellRmSpy.and.callFake(() => { throw 'Test'; });
|
||||
|
||||
(cleaner as any).removeDir('/foo/bar');
|
||||
|
||||
@ -314,28 +287,17 @@ describe('BuildCleaner', () => {
|
||||
it('should construct full paths to directories (by prepending \'buildsDir\')', () => {
|
||||
(cleaner as any).removeUnnecessaryBuilds([1, 2, 3], []);
|
||||
|
||||
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(path.normalize('/foo/bar/1'));
|
||||
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(path.normalize('/foo/bar/2'));
|
||||
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(path.normalize('/foo/bar/3'));
|
||||
});
|
||||
|
||||
|
||||
it('should try removing hidden directories as well', () => {
|
||||
(cleaner as any).removeUnnecessaryBuilds([1, 2, 3], []);
|
||||
|
||||
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(path.normalize(`/foo/bar/${HIDDEN_DIR_PREFIX}1`));
|
||||
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(path.normalize(`/foo/bar/${HIDDEN_DIR_PREFIX}2`));
|
||||
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(path.normalize(`/foo/bar/${HIDDEN_DIR_PREFIX}3`));
|
||||
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(4);
|
||||
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(path.normalize('/foo/bar/1'));
|
||||
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(path.normalize('/foo/bar/3'));
|
||||
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(path.normalize(`/foo/bar/${HIDDEN_DIR_PREFIX}1`));
|
||||
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(path.normalize(`/foo/bar/${HIDDEN_DIR_PREFIX}3`));
|
||||
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]);
|
||||
@ -343,15 +305,11 @@ describe('BuildCleaner', () => {
|
||||
cleanerRemoveDirSpy.calls.reset();
|
||||
|
||||
(cleaner as any).removeUnnecessaryBuilds([1, 2, 3, 4], []);
|
||||
expect(cleanerRemoveDirSpy).toHaveBeenCalledTimes(8);
|
||||
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(path.normalize('/foo/bar/1'));
|
||||
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(path.normalize('/foo/bar/2'));
|
||||
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(path.normalize('/foo/bar/3'));
|
||||
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(path.normalize('/foo/bar/4'));
|
||||
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(path.normalize(`/foo/bar/${HIDDEN_DIR_PREFIX}1`));
|
||||
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(path.normalize(`/foo/bar/${HIDDEN_DIR_PREFIX}2`));
|
||||
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(path.normalize(`/foo/bar/${HIDDEN_DIR_PREFIX}3`));
|
||||
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(path.normalize(`/foo/bar/${HIDDEN_DIR_PREFIX}4`));
|
||||
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();
|
||||
});
|
||||
|
||||
|
@ -56,7 +56,7 @@ describe('GithubApi', () => {
|
||||
|
||||
|
||||
it('should not pass data to \'request()\'', () => {
|
||||
(api.get as any)('foo', {}, {});
|
||||
(api.get as Function)('foo', {}, {});
|
||||
|
||||
expect(apiRequestSpy).toHaveBeenCalled();
|
||||
expect(apiRequestSpy.calls.argsFor(0)[2]).toBeUndefined();
|
||||
@ -144,7 +144,7 @@ describe('GithubApi', () => {
|
||||
|
||||
|
||||
describe('getPaginated()', () => {
|
||||
let deferreds: {resolve: (v: any) => void, reject: (v: any) => void}[];
|
||||
let deferreds: {resolve: Function, reject: Function}[];
|
||||
|
||||
beforeEach(() => {
|
||||
deferreds = [];
|
||||
@ -292,7 +292,7 @@ describe('GithubApi', () => {
|
||||
|
||||
|
||||
describe('onResponse', () => {
|
||||
let promise: Promise<object>;
|
||||
let promise: Promise<void>;
|
||||
let respond: (statusCode: number) => IncomingMessage;
|
||||
|
||||
beforeEach(() => {
|
||||
|
@ -22,7 +22,7 @@ describe('GithubPullRequests', () => {
|
||||
|
||||
describe('addComment()', () => {
|
||||
let prs: GithubPullRequests;
|
||||
let deferred: {resolve: (v: any) => void, reject: (v: any) => void};
|
||||
let deferred: {resolve: Function, reject: Function};
|
||||
|
||||
beforeEach(() => {
|
||||
prs = new GithubPullRequests('12345', 'foo/bar');
|
||||
@ -66,7 +66,7 @@ describe('GithubPullRequests', () => {
|
||||
|
||||
it('should resolve with the returned response', done => {
|
||||
prs.addComment(42, 'body').then(data => {
|
||||
expect(data as any).toBe('Test');
|
||||
expect(data).toEqual('Test');
|
||||
done();
|
||||
});
|
||||
|
||||
@ -76,30 +76,6 @@ describe('GithubPullRequests', () => {
|
||||
});
|
||||
|
||||
|
||||
describe('fetch()', () => {
|
||||
let prs: GithubPullRequests;
|
||||
let prsGetSpy: jasmine.Spy;
|
||||
|
||||
beforeEach(() => {
|
||||
prs = new GithubPullRequests('12345', 'foo/bar');
|
||||
prsGetSpy = spyOn(prs as any, 'get');
|
||||
});
|
||||
|
||||
|
||||
it('should call \'get()\' with the correct pathname', () => {
|
||||
prs.fetch(42);
|
||||
expect(prsGetSpy).toHaveBeenCalledWith('/repos/foo/bar/issues/42');
|
||||
});
|
||||
|
||||
|
||||
it('should forward the value returned by \'get()\'', () => {
|
||||
prsGetSpy.and.returnValue('Test');
|
||||
expect(prs.fetch(42) as any).toBe('Test');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
describe('fetchAll()', () => {
|
||||
let prs: GithubPullRequests;
|
||||
let prsGetPaginatedSpy: jasmine.Spy;
|
||||
@ -133,7 +109,7 @@ describe('GithubPullRequests', () => {
|
||||
|
||||
it('should forward the value returned by \'getPaginated()\'', () => {
|
||||
prsGetPaginatedSpy.and.returnValue('Test');
|
||||
expect(prs.fetchAll() as any).toBe('Test');
|
||||
expect(prs.fetchAll()).toBe('Test');
|
||||
});
|
||||
|
||||
});
|
||||
|
@ -38,7 +38,7 @@ describe('GithubTeams', () => {
|
||||
|
||||
it('should forward the value returned by \'getPaginated()\'', () => {
|
||||
teamsGetPaginatedSpy.and.returnValue('Test');
|
||||
expect(teams.fetchAll() as any).toBe('Test');
|
||||
expect(teams.fetchAll()).toBe('Test');
|
||||
});
|
||||
|
||||
});
|
||||
@ -50,16 +50,12 @@ describe('GithubTeams', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
teams = new GithubTeams('12345', 'foo');
|
||||
teamsGetSpy = spyOn(teams, 'get').and.returnValue(Promise.resolve(null));
|
||||
teamsGetSpy = spyOn(teams, 'get');
|
||||
});
|
||||
|
||||
|
||||
it('should return a promise', done => {
|
||||
const promise = teams.isMemberById('user', [1]);
|
||||
promise.then(done); // Do not complete the test (and release the spies) synchronously
|
||||
// to avoid running the actual `get()`.
|
||||
|
||||
expect(promise).toEqual(jasmine.any(Promise));
|
||||
it('should return a promise', () => {
|
||||
expect(teams.isMemberById('user', [1])).toEqual(jasmine.any(Promise));
|
||||
});
|
||||
|
||||
|
||||
@ -73,6 +69,7 @@ describe('GithubTeams', () => {
|
||||
|
||||
|
||||
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();
|
||||
|
@ -2,11 +2,9 @@
|
||||
import * as cp from 'child_process';
|
||||
import {EventEmitter} from 'events';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as shell from 'shelljs';
|
||||
import {SHORT_SHA_LEN} from '../../lib/common/constants';
|
||||
import {BuildCreator} from '../../lib/upload-server/build-creator';
|
||||
import {ChangedPrVisibilityEvent, CreatedBuildEvent} from '../../lib/upload-server/build-events';
|
||||
import {CreatedBuildEvent} from '../../lib/upload-server/build-events';
|
||||
import {UploadError} from '../../lib/upload-server/upload-error';
|
||||
import {expectToBeUploadError} from './helpers';
|
||||
|
||||
@ -14,13 +12,10 @@ import {expectToBeUploadError} from './helpers';
|
||||
describe('BuildCreator', () => {
|
||||
const pr = '9';
|
||||
const sha = '9'.repeat(40);
|
||||
const shortSha = sha.substr(0, SHORT_SHA_LEN);
|
||||
const archive = 'snapshot.tar.gz';
|
||||
const buildsDir = 'builds/dir';
|
||||
const hiddenPrDir = path.join(buildsDir, `hidden--${pr}`);
|
||||
const publicPrDir = path.join(buildsDir, pr);
|
||||
const hiddenShaDir = path.join(hiddenPrDir, shortSha);
|
||||
const publicShaDir = path.join(publicPrDir, shortSha);
|
||||
const prDir = `${buildsDir}/${pr}`;
|
||||
const shaDir = `${prDir}/${sha}`;
|
||||
let bc: BuildCreator;
|
||||
|
||||
beforeEach(() => bc = new BuildCreator(buildsDir));
|
||||
@ -47,7 +42,6 @@ describe('BuildCreator', () => {
|
||||
let bcEmitSpy: jasmine.Spy;
|
||||
let bcExistsSpy: jasmine.Spy;
|
||||
let bcExtractArchiveSpy: jasmine.Spy;
|
||||
let bcUpdatePrVisibilitySpy: jasmine.Spy;
|
||||
let shellMkdirSpy: jasmine.Spy;
|
||||
let shellRmSpy: jasmine.Spy;
|
||||
|
||||
@ -55,217 +49,13 @@ describe('BuildCreator', () => {
|
||||
bcEmitSpy = spyOn(bc, 'emit');
|
||||
bcExistsSpy = spyOn(bc as any, 'exists');
|
||||
bcExtractArchiveSpy = spyOn(bc as any, 'extractArchive');
|
||||
bcUpdatePrVisibilitySpy = spyOn(bc, 'updatePrVisibility');
|
||||
shellMkdirSpy = spyOn(shell, 'mkdir');
|
||||
shellRmSpy = spyOn(shell, 'rm');
|
||||
});
|
||||
|
||||
|
||||
[true, false].forEach(isPublic => {
|
||||
const prDir = isPublic ? publicPrDir : hiddenPrDir;
|
||||
const shaDir = isPublic ? publicShaDir : hiddenShaDir;
|
||||
|
||||
|
||||
it('should return a promise', done => {
|
||||
const promise = bc.create(pr, sha, archive, isPublic);
|
||||
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 update the PR\'s visibility first if necessary', done => {
|
||||
bcUpdatePrVisibilitySpy.and.callFake(() => expect(shellMkdirSpy).not.toHaveBeenCalled());
|
||||
|
||||
bc.create(pr, sha, archive, isPublic).
|
||||
then(() => {
|
||||
expect(bcUpdatePrVisibilitySpy).toHaveBeenCalledWith(pr, isPublic);
|
||||
expect(shellMkdirSpy).toHaveBeenCalled();
|
||||
}).
|
||||
then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should create the build directory (and any parent directories)', done => {
|
||||
bc.create(pr, sha, archive, isPublic).
|
||||
then(() => expect(shellMkdirSpy).toHaveBeenCalledWith('-p', shaDir)).
|
||||
then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should extract the archive contents into the build directory', done => {
|
||||
bc.create(pr, sha, archive, isPublic).
|
||||
then(() => expect(bcExtractArchiveSpy).toHaveBeenCalledWith(archive, shaDir)).
|
||||
then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should emit a CreatedBuildEvent on success', done => {
|
||||
let emitted = false;
|
||||
|
||||
bcEmitSpy.and.callFake((type: string, evt: CreatedBuildEvent) => {
|
||||
expect(type).toBe(CreatedBuildEvent.type);
|
||||
expect(evt).toEqual(jasmine.any(CreatedBuildEvent));
|
||||
expect(evt.pr).toBe(+pr);
|
||||
expect(evt.sha).toBe(shortSha);
|
||||
expect(evt.isPublic).toBe(isPublic);
|
||||
|
||||
emitted = true;
|
||||
});
|
||||
|
||||
bc.create(pr, sha, archive, isPublic).
|
||||
then(() => expect(emitted).toBe(true)).
|
||||
then(done);
|
||||
});
|
||||
|
||||
|
||||
describe('on error', () => {
|
||||
let existsValues: {[dir: string]: boolean};
|
||||
|
||||
beforeEach(() => {
|
||||
existsValues = {
|
||||
[prDir]: false,
|
||||
[shaDir]: false,
|
||||
};
|
||||
|
||||
bcExistsSpy.and.callFake((dir: string) => existsValues[dir]);
|
||||
});
|
||||
|
||||
|
||||
it('should abort and skip further operations if changing the PR\'s visibility fails', done => {
|
||||
const mockError = new UploadError(543, 'Test');
|
||||
bcUpdatePrVisibilitySpy.and.returnValue(Promise.reject(mockError));
|
||||
|
||||
bc.create(pr, sha, archive, isPublic).catch(err => {
|
||||
expect(err).toBe(mockError);
|
||||
|
||||
expect(bcExistsSpy).not.toHaveBeenCalled();
|
||||
expect(shellMkdirSpy).not.toHaveBeenCalled();
|
||||
expect(bcExtractArchiveSpy).not.toHaveBeenCalled();
|
||||
expect(bcEmitSpy).not.toHaveBeenCalled();
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should abort and skip further operations if the build does already exist', done => {
|
||||
existsValues[shaDir] = true;
|
||||
bc.create(pr, sha, archive, isPublic).catch(err => {
|
||||
const publicOrNot = isPublic ? 'public' : 'non-public';
|
||||
expectToBeUploadError(err, 409, `Request to overwrite existing ${publicOrNot} directory: ${shaDir}`);
|
||||
expect(shellMkdirSpy).not.toHaveBeenCalled();
|
||||
expect(bcExtractArchiveSpy).not.toHaveBeenCalled();
|
||||
expect(bcEmitSpy).not.toHaveBeenCalled();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should detect existing build directory after visibility change', done => {
|
||||
bcUpdatePrVisibilitySpy.and.callFake(() => existsValues[prDir] = existsValues[shaDir] = true);
|
||||
|
||||
expect(bcExistsSpy(prDir)).toBe(false);
|
||||
expect(bcExistsSpy(shaDir)).toBe(false);
|
||||
|
||||
bc.create(pr, sha, archive, isPublic).catch(err => {
|
||||
const publicOrNot = isPublic ? 'public' : 'non-public';
|
||||
expectToBeUploadError(err, 409, `Request to overwrite existing ${publicOrNot} directory: ${shaDir}`);
|
||||
expect(shellMkdirSpy).not.toHaveBeenCalled();
|
||||
expect(bcExtractArchiveSpy).not.toHaveBeenCalled();
|
||||
expect(bcEmitSpy).not.toHaveBeenCalled();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should abort and skip further operations if it fails to create the directories', done => {
|
||||
shellMkdirSpy.and.throwError('');
|
||||
bc.create(pr, sha, archive, isPublic).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, isPublic).catch(() => {
|
||||
expect(shellMkdirSpy).toHaveBeenCalled();
|
||||
expect(bcExtractArchiveSpy).toHaveBeenCalled();
|
||||
expect(bcEmitSpy).not.toHaveBeenCalled();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should delete the PR directory (for new PR)', done => {
|
||||
bcExtractArchiveSpy.and.throwError('');
|
||||
bc.create(pr, sha, archive, isPublic).catch(() => {
|
||||
expect(shellRmSpy).toHaveBeenCalledWith('-rf', prDir);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should delete the SHA directory (for existing PR)', done => {
|
||||
existsValues[prDir] = true;
|
||||
bcExtractArchiveSpy.and.throwError('');
|
||||
|
||||
bc.create(pr, sha, archive, isPublic).catch(() => {
|
||||
expect(shellRmSpy).toHaveBeenCalledWith('-rf', shaDir);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should reject with an UploadError', done => {
|
||||
// tslint:disable-next-line: no-string-throw
|
||||
shellMkdirSpy.and.callFake(() => { throw 'Test'; });
|
||||
bc.create(pr, sha, archive, isPublic).catch(err => {
|
||||
expectToBeUploadError(err, 500, `Error while uploading to directory: ${shaDir}\nTest`);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should pass UploadError instances unmodified', done => {
|
||||
shellMkdirSpy.and.callFake(() => { throw new UploadError(543, 'Test'); });
|
||||
bc.create(pr, sha, archive, isPublic).catch(err => {
|
||||
expectToBeUploadError(err, 543, 'Test');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
describe('updatePrVisibility()', () => {
|
||||
let bcEmitSpy: jasmine.Spy;
|
||||
let bcExistsSpy: jasmine.Spy;
|
||||
let bcListShasByDate: jasmine.Spy;
|
||||
let shellMvSpy: jasmine.Spy;
|
||||
|
||||
beforeEach(() => {
|
||||
bcEmitSpy = spyOn(bc, 'emit');
|
||||
bcExistsSpy = spyOn(bc as any, 'exists');
|
||||
bcListShasByDate = spyOn(bc as any, 'listShasByDate');
|
||||
shellMvSpy = spyOn(shell, 'mv');
|
||||
|
||||
bcExistsSpy.and.returnValues(Promise.resolve(true), Promise.resolve(false));
|
||||
bcListShasByDate.and.returnValue([]);
|
||||
});
|
||||
|
||||
|
||||
it('should return a promise', done => {
|
||||
const promise = bc.updatePrVisibility(pr, true);
|
||||
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()`.
|
||||
|
||||
@ -273,158 +63,106 @@ describe('BuildCreator', () => {
|
||||
});
|
||||
|
||||
|
||||
[true, false].forEach(makePublic => {
|
||||
const oldPrDir = makePublic ? hiddenPrDir : publicPrDir;
|
||||
const newPrDir = makePublic ? publicPrDir : hiddenPrDir;
|
||||
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 rename the directory', done => {
|
||||
bc.updatePrVisibility(pr, makePublic).
|
||||
then(() => expect(shellMvSpy).toHaveBeenCalledWith(oldPrDir, newPrDir)).
|
||||
then(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();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('when the visibility is updated', () => {
|
||||
|
||||
it('should resolve to true', done => {
|
||||
bc.updatePrVisibility(pr, makePublic).
|
||||
then(result => expect(result).toBe(true)).
|
||||
then(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 rename the directory', done => {
|
||||
bc.updatePrVisibility(pr, makePublic).
|
||||
then(() => expect(shellMvSpy).toHaveBeenCalledWith(oldPrDir, newPrDir)).
|
||||
then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should emit a ChangedPrVisibilityEvent on success', done => {
|
||||
let emitted = false;
|
||||
|
||||
bcEmitSpy.and.callFake((type: string, evt: ChangedPrVisibilityEvent) => {
|
||||
expect(type).toBe(ChangedPrVisibilityEvent.type);
|
||||
expect(evt).toEqual(jasmine.any(ChangedPrVisibilityEvent));
|
||||
expect(evt.pr).toBe(+pr);
|
||||
expect(evt.shas).toEqual(jasmine.any(Array));
|
||||
expect(evt.isPublic).toBe(makePublic);
|
||||
|
||||
emitted = true;
|
||||
});
|
||||
|
||||
bc.updatePrVisibility(pr, makePublic).
|
||||
then(() => expect(emitted).toBe(true)).
|
||||
then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should include all shas in the emitted event', done => {
|
||||
const shas = ['foo', 'bar', 'baz'];
|
||||
let emitted = false;
|
||||
|
||||
bcListShasByDate.and.returnValue(Promise.resolve(shas));
|
||||
bcEmitSpy.and.callFake((type: string, evt: ChangedPrVisibilityEvent) => {
|
||||
expect(bcListShasByDate).toHaveBeenCalledWith(newPrDir);
|
||||
|
||||
expect(type).toBe(ChangedPrVisibilityEvent.type);
|
||||
expect(evt).toEqual(jasmine.any(ChangedPrVisibilityEvent));
|
||||
expect(evt.pr).toBe(+pr);
|
||||
expect(evt.shas).toBe(shas);
|
||||
expect(evt.isPublic).toBe(makePublic);
|
||||
|
||||
emitted = true;
|
||||
});
|
||||
|
||||
bc.updatePrVisibility(pr, makePublic).
|
||||
then(() => expect(emitted).toBe(true)).
|
||||
then(done);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
it('should do nothing if the visibility is already up-to-date', done => {
|
||||
bcExistsSpy.and.callFake((dir: string) => dir === newPrDir);
|
||||
bc.updatePrVisibility(pr, makePublic).
|
||||
then(result => {
|
||||
expect(result).toBe(false);
|
||||
expect(shellMvSpy).not.toHaveBeenCalled();
|
||||
expect(bcListShasByDate).not.toHaveBeenCalled();
|
||||
expect(bcEmitSpy).not.toHaveBeenCalled();
|
||||
}).
|
||||
then(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 do nothing if the PR directory does not exist', done => {
|
||||
bcExistsSpy.and.returnValue(false);
|
||||
bc.updatePrVisibility(pr, makePublic).
|
||||
then(result => {
|
||||
expect(result).toBe(false);
|
||||
expect(shellMvSpy).not.toHaveBeenCalled();
|
||||
expect(bcListShasByDate).not.toHaveBeenCalled();
|
||||
expect(bcEmitSpy).not.toHaveBeenCalled();
|
||||
}).
|
||||
then(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();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('on error', () => {
|
||||
|
||||
it('should abort and skip further operations if both directories exist', done => {
|
||||
bcExistsSpy.and.returnValue(true);
|
||||
bc.updatePrVisibility(pr, makePublic).catch(err => {
|
||||
expectToBeUploadError(err, 409, `Request to move '${oldPrDir}' to existing directory '${newPrDir}'.`);
|
||||
expect(shellMvSpy).not.toHaveBeenCalled();
|
||||
expect(bcListShasByDate).not.toHaveBeenCalled();
|
||||
expect(bcEmitSpy).not.toHaveBeenCalled();
|
||||
done();
|
||||
});
|
||||
it('should 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 abort and skip further operations if it fails to rename the directory', done => {
|
||||
shellMvSpy.and.throwError('');
|
||||
bc.updatePrVisibility(pr, makePublic).catch(() => {
|
||||
expect(shellMvSpy).toHaveBeenCalled();
|
||||
expect(bcListShasByDate).not.toHaveBeenCalled();
|
||||
expect(bcEmitSpy).not.toHaveBeenCalled();
|
||||
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();
|
||||
});
|
||||
|
||||
|
||||
it('should abort and skip further operations if it fails to list the SHAs', done => {
|
||||
bcListShasByDate.and.throwError('');
|
||||
bc.updatePrVisibility(pr, makePublic).catch(() => {
|
||||
expect(shellMvSpy).toHaveBeenCalled();
|
||||
expect(bcListShasByDate).toHaveBeenCalled();
|
||||
expect(bcEmitSpy).not.toHaveBeenCalled();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should reject with an UploadError', done => {
|
||||
// tslint:disable-next-line: no-string-throw
|
||||
shellMvSpy.and.callFake(() => { throw 'Test'; });
|
||||
bc.updatePrVisibility(pr, makePublic).catch(err => {
|
||||
expectToBeUploadError(err, 500, `Error while making PR ${pr} ${makePublic ? 'public' : 'hidden'}.\nTest`);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should pass UploadError instances unmodified', done => {
|
||||
shellMvSpy.and.callFake(() => { throw new UploadError(543, 'Test'); });
|
||||
bc.updatePrVisibility(pr, makePublic).catch(err => {
|
||||
expectToBeUploadError(err, 543, 'Test');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
@ -436,11 +174,11 @@ describe('BuildCreator', () => {
|
||||
|
||||
describe('exists()', () => {
|
||||
let fsAccessSpy: jasmine.Spy;
|
||||
let fsAccessCbs: ((v?: any) => void)[];
|
||||
let fsAccessCbs: Function[];
|
||||
|
||||
beforeEach(() => {
|
||||
fsAccessCbs = [];
|
||||
fsAccessSpy = spyOn(fs, 'access').and.callFake((_: string, cb: (v?: any) => void) => fsAccessCbs.push(cb));
|
||||
fsAccessSpy = spyOn(fs, 'access').and.callFake((_: string, cb: Function) => fsAccessCbs.push(cb));
|
||||
});
|
||||
|
||||
|
||||
@ -484,7 +222,7 @@ describe('BuildCreator', () => {
|
||||
let shellChmodSpy: jasmine.Spy;
|
||||
let shellRmSpy: jasmine.Spy;
|
||||
let cpExecSpy: jasmine.Spy;
|
||||
let cpExecCbs: ((...args: any[]) => void)[];
|
||||
let cpExecCbs: Function[];
|
||||
|
||||
beforeEach(() => {
|
||||
cpExecCbs = [];
|
||||
@ -492,7 +230,7 @@ describe('BuildCreator', () => {
|
||||
consoleWarnSpy = spyOn(console, 'warn');
|
||||
shellChmodSpy = spyOn(shell, 'chmod');
|
||||
shellRmSpy = spyOn(shell, 'rm');
|
||||
cpExecSpy = spyOn(cp, 'exec').and.callFake((_: string, cb: (...args: any[]) => void) => cpExecCbs.push(cb));
|
||||
cpExecSpy = spyOn(cp, 'exec').and.callFake((_: string, cb: Function) => cpExecCbs.push(cb));
|
||||
});
|
||||
|
||||
|
||||
@ -558,11 +296,7 @@ describe('BuildCreator', () => {
|
||||
done();
|
||||
});
|
||||
|
||||
shellChmodSpy.and.callFake(() => {
|
||||
// tslint:disable-next-line: no-string-throw
|
||||
throw 'Test';
|
||||
});
|
||||
|
||||
shellChmodSpy.and.callFake(() => { throw 'Test'; });
|
||||
cpExecCbs[0]();
|
||||
});
|
||||
|
||||
@ -575,11 +309,7 @@ describe('BuildCreator', () => {
|
||||
done();
|
||||
});
|
||||
|
||||
shellRmSpy.and.callFake(() => {
|
||||
// tslint:disable-next-line: no-string-throw
|
||||
throw 'Test';
|
||||
});
|
||||
|
||||
shellRmSpy.and.callFake(() => { throw 'Test'; });
|
||||
cpExecCbs[0]();
|
||||
});
|
||||
|
||||
@ -587,101 +317,4 @@ describe('BuildCreator', () => {
|
||||
|
||||
});
|
||||
|
||||
|
||||
describe('listShasByDate()', () => {
|
||||
let shellLsSpy: jasmine.Spy;
|
||||
const lsResult = (name: string, mtimeMs: number, isDirectory = true) => ({
|
||||
isDirectory: () => isDirectory,
|
||||
mtime: new Date(mtimeMs),
|
||||
name,
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
shellLsSpy = spyOn(shell, 'ls').and.returnValue([]);
|
||||
});
|
||||
|
||||
|
||||
it('should return a promise', done => {
|
||||
const promise = (bc as any).listShasByDate('input/dir');
|
||||
promise.then(done); // Do not complete the test (and release the spies) synchronously
|
||||
// to avoid running the actual `ls()`.
|
||||
|
||||
expect(promise).toEqual(jasmine.any(Promise));
|
||||
});
|
||||
|
||||
|
||||
it('should `ls()` files with their metadata', done => {
|
||||
(bc as any).listShasByDate('input/dir').
|
||||
then(() => expect(shellLsSpy).toHaveBeenCalledWith('-l', 'input/dir')).
|
||||
then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should reject if listing files fails', done => {
|
||||
shellLsSpy.and.returnValue(Promise.reject('Test'));
|
||||
(bc as any).listShasByDate('input/dir').catch((err: string) => {
|
||||
expect(err).toBe('Test');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should return the filenames', done => {
|
||||
shellLsSpy.and.returnValue(Promise.resolve([
|
||||
lsResult('foo', 100),
|
||||
lsResult('bar', 200),
|
||||
lsResult('baz', 300),
|
||||
]));
|
||||
|
||||
(bc as any).listShasByDate('input/dir').
|
||||
then((shas: string[]) => expect(shas).toEqual(['foo', 'bar', 'baz'])).
|
||||
then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should sort by date', done => {
|
||||
shellLsSpy.and.returnValue(Promise.resolve([
|
||||
lsResult('foo', 300),
|
||||
lsResult('bar', 100),
|
||||
lsResult('baz', 200),
|
||||
]));
|
||||
|
||||
(bc as any).listShasByDate('input/dir').
|
||||
then((shas: string[]) => expect(shas).toEqual(['bar', 'baz', 'foo'])).
|
||||
then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should not break with ShellJS\' custom `sort()` method', done => {
|
||||
const mockArray = [
|
||||
lsResult('foo', 300),
|
||||
lsResult('bar', 100),
|
||||
lsResult('baz', 200),
|
||||
];
|
||||
mockArray.sort = jasmine.createSpy('sort');
|
||||
|
||||
shellLsSpy.and.returnValue(Promise.resolve(mockArray));
|
||||
(bc as any).listShasByDate('input/dir').
|
||||
then((shas: string[]) => {
|
||||
expect(shas).toEqual(['bar', 'baz', 'foo']);
|
||||
expect(mockArray.sort).not.toHaveBeenCalled();
|
||||
}).
|
||||
then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should only include directories', done => {
|
||||
shellLsSpy.and.returnValue(Promise.resolve([
|
||||
lsResult('foo', 100),
|
||||
lsResult('bar', 200, false),
|
||||
lsResult('baz', 300),
|
||||
]));
|
||||
|
||||
(bc as any).listShasByDate('input/dir').
|
||||
then((shas: string[]) => expect(shas).toEqual(['foo', 'baz'])).
|
||||
then(done);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
@ -1,43 +1,15 @@
|
||||
// Imports
|
||||
import {ChangedPrVisibilityEvent, CreatedBuildEvent} from '../../lib/upload-server/build-events';
|
||||
import {BuildEvent, CreatedBuildEvent} from '../../lib/upload-server/build-events';
|
||||
|
||||
// Tests
|
||||
describe('ChangedPrVisibilityEvent', () => {
|
||||
let evt: ChangedPrVisibilityEvent;
|
||||
describe('BuildEvent', () => {
|
||||
let evt: BuildEvent;
|
||||
|
||||
beforeEach(() => evt = new ChangedPrVisibilityEvent(42, ['foo', 'bar'], true));
|
||||
beforeEach(() => evt = new BuildEvent('foo', 42, 'bar'));
|
||||
|
||||
|
||||
it('should have a static \'type\' property', () => {
|
||||
expect(ChangedPrVisibilityEvent.type).toBe('pr.changedVisibility');
|
||||
});
|
||||
|
||||
|
||||
it('should have a \'pr\' property', () => {
|
||||
expect(evt.pr).toBe(42);
|
||||
});
|
||||
|
||||
|
||||
it('should have a \'shas\' property', () => {
|
||||
expect(evt.shas).toEqual(['foo', 'bar']);
|
||||
});
|
||||
|
||||
|
||||
it('should have an \'isPublic\' property', () => {
|
||||
expect(evt.isPublic).toBe(true);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
describe('CreatedBuildEvent', () => {
|
||||
let evt: CreatedBuildEvent;
|
||||
|
||||
beforeEach(() => evt = new CreatedBuildEvent(42, 'bar', true));
|
||||
|
||||
|
||||
it('should have a static \'type\' property', () => {
|
||||
expect(CreatedBuildEvent.type).toBe('build.created');
|
||||
it('should have a \'type\' property', () => {
|
||||
expect(evt.type).toBe('foo');
|
||||
});
|
||||
|
||||
|
||||
@ -50,9 +22,40 @@ describe('CreatedBuildEvent', () => {
|
||||
expect(evt.sha).toBe('bar');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
it('should have an \'isPublic\' property', () => {
|
||||
expect(evt.isPublic).toBe(true);
|
||||
|
||||
describe('CreatedBuildEvent', () => {
|
||||
let evt: CreatedBuildEvent;
|
||||
|
||||
beforeEach(() => evt = new CreatedBuildEvent(42, 'bar'));
|
||||
|
||||
|
||||
it('should have a static \'type\' property', () => {
|
||||
expect(CreatedBuildEvent.type).toBe('build.created');
|
||||
});
|
||||
|
||||
|
||||
it('should extend BuildEvent', () => {
|
||||
expect(evt).toEqual(jasmine.any(CreatedBuildEvent));
|
||||
expect(evt).toEqual(jasmine.any(BuildEvent));
|
||||
|
||||
expect(Object.getPrototypeOf(evt)).toBe(CreatedBuildEvent.prototype);
|
||||
});
|
||||
|
||||
|
||||
it('should automatically set the \'type\'', () => {
|
||||
expect(evt.type).toBe(CreatedBuildEvent.type);
|
||||
});
|
||||
|
||||
|
||||
it('should have a \'pr\' property', () => {
|
||||
expect(evt.pr).toBe(42);
|
||||
});
|
||||
|
||||
|
||||
it('should have a \'sha\' property', () => {
|
||||
expect(evt.sha).toBe('bar');
|
||||
});
|
||||
|
||||
});
|
||||
|
@ -1,8 +1,8 @@
|
||||
// Imports
|
||||
import * as jwt from 'jsonwebtoken';
|
||||
import {GithubPullRequests, PullRequest} from '../../lib/common/github-pull-requests';
|
||||
import {GithubPullRequests} from '../../lib/common/github-pull-requests';
|
||||
import {GithubTeams} from '../../lib/common/github-teams';
|
||||
import {BUILD_VERIFICATION_STATUS, BuildVerifier} from '../../lib/upload-server/build-verifier';
|
||||
import {BuildVerifier} from '../../lib/upload-server/build-verifier';
|
||||
import {expectToBeUploadError} from './helpers';
|
||||
|
||||
// Tests
|
||||
@ -13,15 +13,14 @@ describe('BuildVerifier', () => {
|
||||
organization: 'organization',
|
||||
repoSlug: 'repo/slug',
|
||||
secret: 'secret',
|
||||
trustedPrLabel: 'trusted: pr-label',
|
||||
};
|
||||
let bv: BuildVerifier;
|
||||
|
||||
// Helpers
|
||||
const createBuildVerifier = (partialConfig: Partial<typeof defaultConfig> = {}) => {
|
||||
const cfg = {...defaultConfig, ...partialConfig} as typeof defaultConfig;
|
||||
const cfg = {...defaultConfig, ...partialConfig};
|
||||
return new BuildVerifier(cfg.secret, cfg.githubToken, cfg.repoSlug, cfg.organization,
|
||||
cfg.allowedTeamSlugs, cfg.trustedPrLabel);
|
||||
cfg.allowedTeamSlugs);
|
||||
};
|
||||
|
||||
beforeEach(() => bv = createBuildVerifier());
|
||||
@ -29,13 +28,12 @@ describe('BuildVerifier', () => {
|
||||
|
||||
describe('constructor()', () => {
|
||||
|
||||
['secret', 'githubToken', 'repoSlug', 'organization', 'allowedTeamSlugs', 'trustedPrLabel'].
|
||||
forEach(param => {
|
||||
it(`should throw if '${param}' is missing or empty`, () => {
|
||||
expect(() => createBuildVerifier({[param]: ''})).
|
||||
toThrowError(`Missing or empty required parameter '${param}'!`);
|
||||
});
|
||||
['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', () => {
|
||||
@ -46,122 +44,6 @@ describe('BuildVerifier', () => {
|
||||
});
|
||||
|
||||
|
||||
describe('getPrIsTrusted()', () => {
|
||||
const pr = 9;
|
||||
let mockPrInfo: PullRequest;
|
||||
let prsFetchSpy: jasmine.Spy;
|
||||
let teamsIsMemberBySlugSpy: jasmine.Spy;
|
||||
|
||||
beforeEach(() => {
|
||||
mockPrInfo = {
|
||||
labels: [
|
||||
{name: 'foo'},
|
||||
{name: 'bar'},
|
||||
],
|
||||
number: 9,
|
||||
user: {login: 'username'},
|
||||
};
|
||||
|
||||
prsFetchSpy = spyOn(GithubPullRequests.prototype, 'fetch').
|
||||
and.returnValue(Promise.resolve(mockPrInfo));
|
||||
|
||||
teamsIsMemberBySlugSpy = spyOn(GithubTeams.prototype, 'isMemberBySlug').
|
||||
and.returnValue(Promise.resolve(true));
|
||||
});
|
||||
|
||||
|
||||
it('should return a promise', done => {
|
||||
const promise = bv.getPrIsTrusted(pr);
|
||||
promise.then(done); // Do not complete the test (and release the spies) synchronously
|
||||
// to avoid running the actual `GithubTeams#isMemberBySlug()`.
|
||||
|
||||
expect(promise).toEqual(jasmine.any(Promise));
|
||||
});
|
||||
|
||||
|
||||
it('should fetch the corresponding PR', done => {
|
||||
bv.getPrIsTrusted(pr).then(() => {
|
||||
expect(prsFetchSpy).toHaveBeenCalledWith(pr);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should fail if fetching the PR errors', done => {
|
||||
prsFetchSpy.and.callFake(() => Promise.reject('Test'));
|
||||
bv.getPrIsTrusted(pr).catch(err => {
|
||||
expect(err).toBe('Test');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('when the PR has the "trusted PR" label', () => {
|
||||
|
||||
beforeEach(() => mockPrInfo.labels.push({name: 'trusted: pr-label'}));
|
||||
|
||||
|
||||
it('should resolve to true', done => {
|
||||
bv.getPrIsTrusted(pr).then(isTrusted => {
|
||||
expect(isTrusted).toBe(true);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should not try to verify the author\'s membership status', done => {
|
||||
bv.getPrIsTrusted(pr).then(() => {
|
||||
expect(teamsIsMemberBySlugSpy).not.toHaveBeenCalled();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
describe('when the PR does not have the "trusted PR" label', () => {
|
||||
|
||||
it('should verify the PR author\'s membership in the specified teams', done => {
|
||||
bv.getPrIsTrusted(pr).then(() => {
|
||||
expect(teamsIsMemberBySlugSpy).toHaveBeenCalledWith('username', ['team1', 'team2']);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should fail if verifying membership errors', done => {
|
||||
teamsIsMemberBySlugSpy.and.callFake(() => Promise.reject('Test'));
|
||||
bv.getPrIsTrusted(pr).catch(err => {
|
||||
expect(err).toBe('Test');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should resolve to true if the PR\'s author is a member', done => {
|
||||
teamsIsMemberBySlugSpy.and.returnValue(Promise.resolve(true));
|
||||
|
||||
bv.getPrIsTrusted(pr).then(isTrusted => {
|
||||
expect(isTrusted).toBe(true);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should resolve to false if the PR\'s author is not a member', done => {
|
||||
teamsIsMemberBySlugSpy.and.returnValue(Promise.resolve(false));
|
||||
|
||||
bv.getPrIsTrusted(pr).then(isTrusted => {
|
||||
expect(isTrusted).toBe(false);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
describe('verify()', () => {
|
||||
const pr = 9;
|
||||
const defaultJwt = {
|
||||
@ -171,21 +53,22 @@ describe('BuildVerifier', () => {
|
||||
'pull-request': pr,
|
||||
'slug': defaultConfig.repoSlug,
|
||||
};
|
||||
let bvGetPrIsTrusted: jasmine.Spy;
|
||||
let bvGetPrAuthorTeamMembership: jasmine.Spy;
|
||||
|
||||
// Heleprs
|
||||
const createAuthHeader = (partialJwt: Partial<typeof defaultJwt> = {}, secret: string = defaultConfig.secret) =>
|
||||
`Token ${jwt.sign({...defaultJwt, ...partialJwt}, secret)}`;
|
||||
|
||||
beforeEach(() => {
|
||||
bvGetPrIsTrusted = spyOn(bv, 'getPrIsTrusted').and.returnValue(Promise.resolve(true));
|
||||
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 `bvGetPrIsTrusted()`.
|
||||
// to avoid running the actual `bvGetPrAuthorTeamMembership()`.
|
||||
|
||||
expect(promise).toEqual(jasmine.any(Promise));
|
||||
});
|
||||
@ -265,16 +148,16 @@ describe('BuildVerifier', () => {
|
||||
});
|
||||
|
||||
|
||||
it('should call \'getPrIsTrusted()\' if the token is valid', done => {
|
||||
it('should call \'getPrAuthorTeamMembership()\' if the token is valid', done => {
|
||||
bv.verify(pr, createAuthHeader()).then(() => {
|
||||
expect(bvGetPrIsTrusted).toHaveBeenCalledWith(pr);
|
||||
expect(bvGetPrAuthorTeamMembership).toHaveBeenCalledWith(pr);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should fail if \'getPrIsTrusted()\' rejects', done => {
|
||||
bvGetPrIsTrusted.and.callFake(() => Promise.reject('Test'));
|
||||
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();
|
||||
@ -282,22 +165,97 @@ describe('BuildVerifier', () => {
|
||||
});
|
||||
|
||||
|
||||
it('should resolve to `verifiedNotTrusted` if \'getPrIsTrusted()\' returns false', done => {
|
||||
bvGetPrIsTrusted.and.returnValue(Promise.resolve(false));
|
||||
bv.verify(pr, createAuthHeader()).then(value => {
|
||||
expect(value).toBe(BUILD_VERIFICATION_STATUS.verifiedNotTrusted);
|
||||
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 resolve to `verifiedAndTrusted` if \'getPrIsTrusted()\' returns true', done => {
|
||||
bv.verify(pr, createAuthHeader()).then(value => {
|
||||
expect(value).toBe(BUILD_VERIFICATION_STATUS.verifiedAndTrusted);
|
||||
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);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
@ -4,8 +4,8 @@ import * as http from 'http';
|
||||
import * as supertest from 'supertest';
|
||||
import {GithubPullRequests} from '../../lib/common/github-pull-requests';
|
||||
import {BuildCreator} from '../../lib/upload-server/build-creator';
|
||||
import {ChangedPrVisibilityEvent, CreatedBuildEvent} from '../../lib/upload-server/build-events';
|
||||
import {BUILD_VERIFICATION_STATUS, BuildVerifier} from '../../lib/upload-server/build-verifier';
|
||||
import {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
|
||||
@ -18,12 +18,11 @@ describe('uploadServerFactory', () => {
|
||||
githubToken: '12345',
|
||||
repoSlug: 'repo/slug',
|
||||
secret: 'secret',
|
||||
trustedPrLabel: 'trusted: pr-label',
|
||||
};
|
||||
|
||||
// Helpers
|
||||
const createUploadServer = (partialConfig: Partial<typeof defaultConfig> = {}) =>
|
||||
usf.create({...defaultConfig, ...partialConfig} as typeof defaultConfig);
|
||||
usf.create({...defaultConfig, ...partialConfig});
|
||||
|
||||
|
||||
describe('create()', () => {
|
||||
@ -76,12 +75,6 @@ describe('uploadServerFactory', () => {
|
||||
});
|
||||
|
||||
|
||||
it('should throw if \'trustedPrLabel\' is missing or empty', () => {
|
||||
expect(() => createUploadServer({trustedPrLabel: ''})).
|
||||
toThrowError('Missing or empty required parameter \'trustedPrLabel\'!');
|
||||
});
|
||||
|
||||
|
||||
it('should return an http.Server', () => {
|
||||
const httpCreateServerSpy = spyOn(http, 'createServer').and.callThrough();
|
||||
const server = createUploadServer();
|
||||
@ -116,7 +109,7 @@ describe('uploadServerFactory', () => {
|
||||
|
||||
it('should log the server address info on \'listening\'', () => {
|
||||
const consoleInfoSpy = spyOn(console, 'info');
|
||||
const server = createUploadServer();
|
||||
const server = createUploadServer('builds/dir');
|
||||
server.address = () => ({address: 'foo', family: '', port: 1337});
|
||||
|
||||
expect(consoleInfoSpy).not.toHaveBeenCalled();
|
||||
@ -148,71 +141,26 @@ describe('uploadServerFactory', () => {
|
||||
});
|
||||
|
||||
|
||||
describe('on \'build.created\'', () => {
|
||||
let prsAddCommentSpy: jasmine.Spy;
|
||||
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/';
|
||||
|
||||
beforeEach(() => prsAddCommentSpy = spyOn(GithubPullRequests.prototype, 'addComment'));
|
||||
|
||||
|
||||
it('should post a comment on GitHub for public previews', () => {
|
||||
const commentBody = 'You can preview 1234567890 at https://pr42-1234567890.domain.name/.';
|
||||
|
||||
buildCreator.emit(CreatedBuildEvent.type, {pr: 42, sha: '1234567890', isPublic: true});
|
||||
expect(prsAddCommentSpy).toHaveBeenCalledWith(42, commentBody);
|
||||
});
|
||||
|
||||
|
||||
it('should not post a comment on GitHub for non-public previews', () => {
|
||||
buildCreator.emit(CreatedBuildEvent.type, {pr: 42, sha: '1234567890', isPublic: false});
|
||||
expect(prsAddCommentSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
describe('on \'pr.changedVisibility\'', () => {
|
||||
let prsAddCommentSpy: jasmine.Spy;
|
||||
|
||||
beforeEach(() => prsAddCommentSpy = spyOn(GithubPullRequests.prototype, 'addComment'));
|
||||
|
||||
|
||||
it('should post a comment on GitHub (for all SHAs) for PRs made public', () => {
|
||||
const commentBody = 'You can preview 12345 at https://pr42-12345.domain.name/.\n' +
|
||||
'You can preview 67890 at https://pr42-67890.domain.name/.';
|
||||
|
||||
buildCreator.emit(ChangedPrVisibilityEvent.type, {pr: 42, shas: ['12345', '67890'], isPublic: true});
|
||||
expect(prsAddCommentSpy).toHaveBeenCalledWith(42, commentBody);
|
||||
});
|
||||
|
||||
|
||||
it('should not post a comment on GitHub if no SHAs were affected', () => {
|
||||
buildCreator.emit(ChangedPrVisibilityEvent.type, {pr: 42, shas: [], isPublic: true});
|
||||
expect(prsAddCommentSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
|
||||
it('should not post a comment on GitHub for PRs made non-public', () => {
|
||||
buildCreator.emit(ChangedPrVisibilityEvent.type, {pr: 42, shas: ['12345', '67890'], isPublic: false});
|
||||
expect(prsAddCommentSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
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', isPublic: true});
|
||||
buildCreator.emit(ChangedPrVisibilityEvent.type, {pr: 42, shas: ['12345', '67890'], isPublic: true});
|
||||
buildCreator.emit(CreatedBuildEvent.type, {pr: 42, sha: '1234567890'});
|
||||
const prs = prsAddCommentSpy.calls.mostRecent().object;
|
||||
|
||||
const allCalls = prsAddCommentSpy.calls.all();
|
||||
const prs = allCalls[0].object;
|
||||
|
||||
expect(prsAddCommentSpy).toHaveBeenCalledTimes(2);
|
||||
expect(prs).toBe(allCalls[1].object);
|
||||
expect(prs).toEqual(jasmine.any(GithubPullRequests));
|
||||
expect(prs.repoSlug).toBe('repo/slug');
|
||||
expect(prs.requestHeaders.Authorization).toContain('12345');
|
||||
expect((prs as any).repoSlug).toBe('repo/slug');
|
||||
expect((prs as any).requestHeaders.Authorization).toContain('12345');
|
||||
});
|
||||
|
||||
});
|
||||
@ -236,7 +184,6 @@ describe('uploadServerFactory', () => {
|
||||
defaultConfig.repoSlug,
|
||||
defaultConfig.githubOrganization,
|
||||
defaultConfig.githubTeamSlugs,
|
||||
defaultConfig.trustedPrLabel,
|
||||
);
|
||||
buildCreator = new BuildCreator(defaultConfig.buildsDir);
|
||||
agent = supertest.agent((usf as any).createMiddleware(buildVerifier, buildCreator));
|
||||
@ -252,18 +199,17 @@ describe('uploadServerFactory', () => {
|
||||
let buildCreatorCreateSpy: jasmine.Spy;
|
||||
|
||||
beforeEach(() => {
|
||||
const verStatus = BUILD_VERIFICATION_STATUS.verifiedAndTrusted;
|
||||
buildVerifierVerifySpy = spyOn(buildVerifier, 'verify').and.returnValue(Promise.resolve(verStatus));
|
||||
buildVerifierVerifySpy = spyOn(buildVerifier, 'verify').and.returnValue(Promise.resolve());
|
||||
buildCreatorCreateSpy = spyOn(buildCreator, 'create').and.returnValue(Promise.resolve());
|
||||
});
|
||||
|
||||
|
||||
it('should respond with 404 for non-GET requests', done => {
|
||||
it('should respond with 405 for non-GET requests', done => {
|
||||
verifyRequests([
|
||||
agent.put(`/create-build/${pr}/${sha}`).expect(404),
|
||||
agent.post(`/create-build/${pr}/${sha}`).expect(404),
|
||||
agent.patch(`/create-build/${pr}/${sha}`).expect(404),
|
||||
agent.delete(`/create-build/${pr}/${sha}`).expect(404),
|
||||
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);
|
||||
});
|
||||
|
||||
@ -338,17 +284,14 @@ describe('uploadServerFactory', () => {
|
||||
|
||||
|
||||
it('should call \'BuildCreator#create()\' with the correct arguments', done => {
|
||||
buildVerifierVerifySpy.and.returnValues(
|
||||
Promise.resolve(BUILD_VERIFICATION_STATUS.verifiedAndTrusted),
|
||||
Promise.resolve(BUILD_VERIFICATION_STATUS.verifiedNotTrusted));
|
||||
const req = agent.
|
||||
get(`/create-build/${pr}/${sha}`).
|
||||
set('AUTHORIZATION', 'foo').
|
||||
set('X-FILE', 'bar');
|
||||
|
||||
const req1 = agent.get(`/create-build/${pr}/${sha}`).set('AUTHORIZATION', 'foo').set('X-FILE', 'bar');
|
||||
const req2 = agent.get(`/create-build/${pr}/${sha}`).set('AUTHORIZATION', 'foo').set('X-FILE', 'bar');
|
||||
|
||||
Promise.all([
|
||||
promisifyRequest(req1).then(() => expect(buildCreatorCreateSpy).toHaveBeenCalledWith(pr, sha, 'bar', true)),
|
||||
promisifyRequest(req2).then(() => expect(buildCreatorCreateSpy).toHaveBeenCalledWith(pr, sha, 'bar', false)),
|
||||
]).then(done, done.fail);
|
||||
promisifyRequest(req).
|
||||
then(() => expect(buildCreatorCreateSpy).toHaveBeenCalledWith(pr, sha, 'bar')).
|
||||
then(done, done.fail);
|
||||
});
|
||||
|
||||
|
||||
@ -364,7 +307,7 @@ describe('uploadServerFactory', () => {
|
||||
});
|
||||
|
||||
|
||||
it('should respond with 201 on successful upload (for public builds)', done => {
|
||||
it('should respond with 201 on successful upload', done => {
|
||||
const req = agent.
|
||||
get(`/create-build/${pr}/${sha}`).
|
||||
set('AUTHORIZATION', 'foo').
|
||||
@ -375,18 +318,6 @@ describe('uploadServerFactory', () => {
|
||||
});
|
||||
|
||||
|
||||
it('should respond with 202 on successful upload (for hidden builds)', done => {
|
||||
buildVerifierVerifySpy.and.returnValue(Promise.resolve(BUILD_VERIFICATION_STATUS.verifiedNotTrusted));
|
||||
const req = agent.
|
||||
get(`/create-build/${pr}/${sha}`).
|
||||
set('AUTHORIZATION', 'foo').
|
||||
set('X-FILE', 'bar').
|
||||
expect(202, http.STATUS_CODES[202]);
|
||||
|
||||
verifyRequests([req], done);
|
||||
});
|
||||
|
||||
|
||||
it('should reject PRs with leading zeros', done => {
|
||||
verifyRequests([agent.get(`/create-build/0${pr}/${sha}`).expect(404)], done);
|
||||
});
|
||||
@ -418,12 +349,12 @@ describe('uploadServerFactory', () => {
|
||||
});
|
||||
|
||||
|
||||
it('should respond with 404 for non-GET requests', done => {
|
||||
it('should respond with 405 for non-GET requests', done => {
|
||||
verifyRequests([
|
||||
agent.put('/health-check').expect(404),
|
||||
agent.post('/health-check').expect(404),
|
||||
agent.patch('/health-check').expect(404),
|
||||
agent.delete('/health-check').expect(404),
|
||||
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);
|
||||
});
|
||||
|
||||
@ -442,141 +373,11 @@ describe('uploadServerFactory', () => {
|
||||
});
|
||||
|
||||
|
||||
describe('POST /pr-updated', () => {
|
||||
const pr = '9';
|
||||
const url = '/pr-updated';
|
||||
let bvGetPrIsTrustedSpy: jasmine.Spy;
|
||||
let bcUpdatePrVisibilitySpy: jasmine.Spy;
|
||||
|
||||
// Helpers
|
||||
const createRequest = (num: number, action?: string) =>
|
||||
agent.post(url).send({number: num, action});
|
||||
|
||||
beforeEach(() => {
|
||||
bvGetPrIsTrustedSpy = spyOn(buildVerifier, 'getPrIsTrusted');
|
||||
bcUpdatePrVisibilitySpy = spyOn(buildCreator, 'updatePrVisibility');
|
||||
});
|
||||
|
||||
|
||||
it('should respond with 404 for non-POST requests', done => {
|
||||
verifyRequests([
|
||||
agent.get(url).expect(404),
|
||||
agent.put(url).expect(404),
|
||||
agent.patch(url).expect(404),
|
||||
agent.delete(url).expect(404),
|
||||
], done);
|
||||
});
|
||||
|
||||
|
||||
it('should respond with 400 for requests without a payload', done => {
|
||||
const responseBody = `Missing or empty 'number' field in request: POST ${url} {}`;
|
||||
|
||||
const request1 = agent.post(url);
|
||||
const request2 = agent.post(url).send();
|
||||
|
||||
verifyRequests([
|
||||
request1.expect(400, responseBody),
|
||||
request2.expect(400, responseBody),
|
||||
], done);
|
||||
});
|
||||
|
||||
|
||||
it('should respond with 400 for requests without a \'number\' field', done => {
|
||||
const responseBodyPrefix = `Missing or empty 'number' field in request: POST ${url}`;
|
||||
|
||||
const request1 = agent.post(url).send({});
|
||||
const request2 = agent.post(url).send({number: null});
|
||||
|
||||
verifyRequests([
|
||||
request1.expect(400, `${responseBodyPrefix} {}`),
|
||||
request2.expect(400, `${responseBodyPrefix} {"number":null}`),
|
||||
], done);
|
||||
});
|
||||
|
||||
|
||||
it('should call \'BuildVerifier#gtPrIsTrusted()\' with the correct arguments', done => {
|
||||
const req = createRequest(+pr);
|
||||
|
||||
promisifyRequest(req).
|
||||
then(() => expect(bvGetPrIsTrustedSpy).toHaveBeenCalledWith(9)).
|
||||
then(done, done.fail);
|
||||
});
|
||||
|
||||
|
||||
it('should propagate errors from BuildVerifier', done => {
|
||||
bvGetPrIsTrustedSpy.and.callFake(() => Promise.reject('Test'));
|
||||
|
||||
const req = createRequest(+pr).expect(500, 'Test');
|
||||
|
||||
promisifyRequest(req).
|
||||
then(() => {
|
||||
expect(bvGetPrIsTrustedSpy).toHaveBeenCalledWith(9);
|
||||
expect(bcUpdatePrVisibilitySpy).not.toHaveBeenCalled();
|
||||
}).
|
||||
then(done, done.fail);
|
||||
});
|
||||
|
||||
|
||||
it('should call \'BuildCreator#updatePrVisibility()\' with the correct arguments', done => {
|
||||
bvGetPrIsTrustedSpy.and.callFake((pr2: number) => Promise.resolve(pr2 === 42));
|
||||
|
||||
const req1 = createRequest(24);
|
||||
const req2 = createRequest(42);
|
||||
|
||||
Promise.all([
|
||||
promisifyRequest(req1).then(() => expect(bcUpdatePrVisibilitySpy).toHaveBeenCalledWith('24', false)),
|
||||
promisifyRequest(req2).then(() => expect(bcUpdatePrVisibilitySpy).toHaveBeenCalledWith('42', true)),
|
||||
]).then(done, done.fail);
|
||||
});
|
||||
|
||||
|
||||
it('should propagate errors from BuildCreator', done => {
|
||||
bcUpdatePrVisibilitySpy.and.callFake(() => Promise.reject('Test'));
|
||||
|
||||
const req = createRequest(+pr).expect(500, 'Test');
|
||||
verifyRequests([req], done);
|
||||
});
|
||||
|
||||
|
||||
describe('on success', () => {
|
||||
|
||||
it('should respond with 200 (action: undefined)', done => {
|
||||
bvGetPrIsTrustedSpy.and.returnValues(Promise.resolve(true), Promise.resolve(false));
|
||||
|
||||
const reqs = [4, 2].map(num => createRequest(num).expect(200, http.STATUS_CODES[200]));
|
||||
verifyRequests(reqs, done);
|
||||
});
|
||||
|
||||
|
||||
it('should respond with 200 (action: labeled)', done => {
|
||||
bvGetPrIsTrustedSpy.and.returnValues(Promise.resolve(true), Promise.resolve(false));
|
||||
|
||||
const reqs = [4, 2].map(num => createRequest(num, 'labeled').expect(200, http.STATUS_CODES[200]));
|
||||
verifyRequests(reqs, done);
|
||||
});
|
||||
|
||||
|
||||
it('should respond with 200 (action: unlabeled)', done => {
|
||||
bvGetPrIsTrustedSpy.and.returnValues(Promise.resolve(true), Promise.resolve(false));
|
||||
|
||||
const reqs = [4, 2].map(num => createRequest(num, 'unlabeled').expect(200, http.STATUS_CODES[200]));
|
||||
verifyRequests(reqs, done);
|
||||
});
|
||||
|
||||
|
||||
it('should respond with 200 (and do nothing) if \'action\' implies no visibility change', done => {
|
||||
const promises = ['foo', 'notlabeled'].
|
||||
map(action => createRequest(+pr, action).expect(200, http.STATUS_CODES[200])).
|
||||
map(promisifyRequest);
|
||||
|
||||
Promise.all(promises).
|
||||
then(() => {
|
||||
expect(bvGetPrIsTrustedSpy).not.toHaveBeenCalled();
|
||||
expect(bcUpdatePrVisibilitySpy).not.toHaveBeenCalled();
|
||||
}).
|
||||
then(done, done.fail);
|
||||
});
|
||||
describe('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);
|
||||
});
|
||||
|
||||
});
|
||||
@ -584,15 +385,14 @@ describe('uploadServerFactory', () => {
|
||||
|
||||
describe('ALL *', () => {
|
||||
|
||||
it('should respond with 404', done => {
|
||||
const responseFor = (method: string) => `Unknown resource in request: ${method.toUpperCase()} /some/url`;
|
||||
it('should respond with 405', done => {
|
||||
const responseFor = (method: string) => `Unsupported method in request: ${method.toUpperCase()} /some/url`;
|
||||
|
||||
verifyRequests([
|
||||
agent.get('/some/url').expect(404, responseFor('get')),
|
||||
agent.put('/some/url').expect(404, responseFor('put')),
|
||||
agent.post('/some/url').expect(404, responseFor('post')),
|
||||
agent.patch('/some/url').expect(404, responseFor('patch')),
|
||||
agent.delete('/some/url').expect(404, responseFor('delete')),
|
||||
agent.put('/some/url').expect(405, responseFor('put')),
|
||||
agent.post('/some/url').expect(405, responseFor('post')),
|
||||
agent.patch('/some/url').expect(405, responseFor('patch')),
|
||||
agent.delete('/some/url').expect(405, responseFor('delete')),
|
||||
], done);
|
||||
});
|
||||
|
||||
|
@ -1,66 +1,25 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
/* Basic Options */
|
||||
"target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', or 'ESNEXT'. */
|
||||
"module": "commonjs", /* Specify module code generation: 'none', commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
|
||||
"alwaysStrict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"inlineSourceMap": true,
|
||||
"lib": [
|
||||
"es2015",
|
||||
"es2016.array.include"
|
||||
], /* Specify library files to be included in the compilation: */
|
||||
// "allowJs": true, /* Allow javascript files to be compiled. */
|
||||
// "checkJs": true, /* Report errors in .js files. */
|
||||
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
|
||||
// "declaration": true, /* Generates corresponding '.d.ts' file. */
|
||||
// "sourceMap": true, /* Generates corresponding '.map' file. */
|
||||
// "outFile": "./", /* Concatenate and emit output to single file. */
|
||||
"outDir": "dist", /* Redirect output structure to the directory. */
|
||||
// "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
|
||||
// "removeComments": true, /* Do not emit comments to output. */
|
||||
// "noEmit": true, /* Do not emit outputs. */
|
||||
"importHelpers": true, /* Import emit helpers from 'tslib'. */
|
||||
// "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
|
||||
// "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
|
||||
|
||||
/* Strict Type-Checking Options */
|
||||
"strict": true, /* Enable all strict type-checking options. */
|
||||
// "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
|
||||
// "strictNullChecks": true, /* Enable strict null checks. */
|
||||
// "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
|
||||
// "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
|
||||
|
||||
/* Additional Checks */
|
||||
"noUnusedLocals": true, /* Report errors on unused locals. */
|
||||
"noUnusedParameters": true, /* Report errors on unused parameters. */
|
||||
"noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
|
||||
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
|
||||
|
||||
/* Module Resolution Options */
|
||||
// "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
|
||||
// "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
|
||||
// "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
|
||||
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
|
||||
"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"
|
||||
], /* List of folders to include type definitions from. */
|
||||
// "types": [], /* Type declaration files to be included in compilation. */
|
||||
// "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
|
||||
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
|
||||
|
||||
/* Source Map Options */
|
||||
// "sourceRoot": "./", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
|
||||
// "mapRoot": "./", /* Specify the location where debugger should locate map files instead of generated locations. */
|
||||
"inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
|
||||
// "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
|
||||
|
||||
/* Experimental Options */
|
||||
// "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
|
||||
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
|
||||
|
||||
/* Other */
|
||||
"forceConsistentCasingInFileNames": true, /* Disallow inconsistently-cased references to the same file. */
|
||||
"newLine": "LF", /* Use the specified end of line sequence to be used when emitting files: "crlf" (windows) or "lf" (unix). */
|
||||
"pretty": true, /* Stylize errors and messages using color and context. */
|
||||
"skipLibCheck": true /* Skip type checking of all declaration files (*.d.ts). */
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"lib/**/*",
|
||||
|
@ -1,24 +1,15 @@
|
||||
{
|
||||
"defaultSeverity": "error",
|
||||
"extends": [
|
||||
"tslint:recommended"
|
||||
],
|
||||
"jsRules": {},
|
||||
"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-focused-test": true,
|
||||
"no-console": false,
|
||||
"no-namespace": [true, "allow-declarations"],
|
||||
"no-skipped-test": true,
|
||||
"no-string-literal": false,
|
||||
"quotemark": [true, "single"],
|
||||
"variable-name": [true, "ban-keywords", "check-format", "allow-leading-underscore"]
|
||||
},
|
||||
"rulesDirectory": [
|
||||
"node_modules/tslint-jasmine-noSkipOrFocus"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -1,5 +1,5 @@
|
||||
#!/bin/bash
|
||||
set -eu -o pipefail
|
||||
set -e -o pipefail
|
||||
|
||||
# Set up env variables
|
||||
export AIO_GITHUB_TOKEN=$(head -c -1 /aio-secrets/GITHUB_TOKEN 2>/dev/null)
|
||||
|
@ -1,6 +1,5 @@
|
||||
#!/bin/bash
|
||||
# Using `+e` so that all checks are run and we get a complete report (even if some checks failed).
|
||||
set +e -u -o pipefail
|
||||
set +e -o pipefail
|
||||
|
||||
|
||||
# Variables
|
||||
|
@ -1,5 +1,5 @@
|
||||
#!/bin/bash
|
||||
set -eu -o pipefail
|
||||
set -e -o pipefail
|
||||
|
||||
exec >> /var/log/aio/init.log
|
||||
exec 2>&1
|
||||
|
@ -1,14 +1,15 @@
|
||||
#!/bin/bash
|
||||
set -eu -o pipefail
|
||||
set -e -o pipefail
|
||||
|
||||
# Set up env variables for production
|
||||
export AIO_GITHUB_TOKEN=$(head -c -1 /aio-secrets/GITHUB_TOKEN 2>/dev/null || echo "MISSING_GITHUB_TOKEN")
|
||||
export AIO_PREVIEW_DEPLOYMENT_TOKEN=$(head -c -1 /aio-secrets/PREVIEW_DEPLOYMENT_TOKEN 2>/dev/null || echo "MISSING_PREVIEW_DEPLOYMENT_TOKEN")
|
||||
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 \
|
||||
--uid $AIO_WWW_USER \
|
||||
--log /var/log/aio/upload-server-prod.log \
|
||||
--name aio-upload-server-prod \
|
||||
${@:2}
|
||||
|
@ -1,13 +1,13 @@
|
||||
#!/bin/bash
|
||||
set -eu -o pipefail
|
||||
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_TRUSTED_PR_LABEL=$TEST_AIO_TRUSTED_PR_LABEL
|
||||
export AIO_UPLOAD_HOSTNAME=$TEST_AIO_UPLOAD_HOSTNAME
|
||||
export AIO_UPLOAD_PORT=$TEST_AIO_UPLOAD_PORT
|
||||
|
||||
@ -15,12 +15,13 @@ export AIO_GITHUB_TOKEN=$(head -c -1 /aio-secrets/TEST_GITHUB_TOKEN 2>/dev/null
|
||||
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/verify-setup/start-test-upload-server.js \
|
||||
--uid $AIO_WWW_USER \
|
||||
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 \
|
||||
|
@ -1,5 +1,5 @@
|
||||
#!/bin/bash
|
||||
set -eu -o pipefail
|
||||
set -e -o pipefail
|
||||
|
||||
logFile=/var/log/aio/verify-setup.log
|
||||
uploadServerLogFile=/var/log/aio/upload-server-verify-setup.log
|
||||
|
@ -4,8 +4,7 @@
|
||||
## Overview
|
||||
- [General overview](overview--general.md)
|
||||
- [Security model](overview--security-model.md)
|
||||
- [Available scripts and commands](overview--scripts-and-commands.md)
|
||||
- [HTTP status codes](overview--http-status-codes.md)
|
||||
- [Available Commands](overview--scripts-and-commands.md)
|
||||
|
||||
|
||||
## Setting up the VM
|
||||
@ -21,11 +20,7 @@
|
||||
|
||||
|
||||
## Starting the docker container
|
||||
- [Start docker container](vm-setup--start-docker-container.md)
|
||||
|
||||
|
||||
## Updating the docker container
|
||||
- [Update docker container](vm-setup--update-docker-container.md)
|
||||
- [Create docker image](vm-setup--start-docker-container.md)
|
||||
|
||||
|
||||
## Miscellaneous
|
||||
|
@ -17,7 +17,7 @@ you don't need to specify values for those.
|
||||
The domain name of the server.
|
||||
|
||||
- `AIO_GITHUB_ORGANIZATION`:
|
||||
The GitHub organization whose teams are whitelisted for accepting uploads.
|
||||
The GitHub organization whose teams arew whitelisted for accepting uploads.
|
||||
See also `AIO_GITHUB_TEAM_SLUGS`.
|
||||
|
||||
- `AIO_GITHUB_TEAM_SLUGS`:
|
||||
@ -39,11 +39,6 @@ you don't need to specify values for those.
|
||||
- `AIO_REPO_SLUG`:
|
||||
The repository slug (in the form `<user>/<repo>`) for which PRs will be uploaded.
|
||||
|
||||
- `AIO_TRUSTED_PR_LABEL`:
|
||||
The PR whose presence indicates the PR has been manually verified and is allowed to have its
|
||||
build artifacts publicly served. This is useful for enabling previews for any PR (not only those
|
||||
from trusted authors).
|
||||
|
||||
- `AIO_UPLOAD_HOSTNAME`:
|
||||
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.
|
||||
|
@ -3,9 +3,10 @@
|
||||
|
||||
TODO (gkalpak): Add docs. Mention:
|
||||
- Travis' JWT addon (+ limitations).
|
||||
Relevant files: `.travis.yml`, `scripts/ci/env.sh`
|
||||
Relevant files: `.travis.yml`
|
||||
- Testing on CI.
|
||||
Relevant files: `scripts/ci/test-aio.sh`, `aio/aio-builds-setup/scripts/test.sh`
|
||||
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: `scripts/ci/deploy.sh`, `aio/scripts/deploy-preview.sh`,
|
||||
`aio/scripts/deploy-to-firebase.sh`
|
||||
Relevant files: `ci/deploy.sh`, `aio/scripts/deploy-preview.sh`
|
||||
|
@ -33,69 +33,36 @@ container:
|
||||
|
||||
|
||||
### On CI (Travis)
|
||||
- Build job completes successfully.
|
||||
- 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 that might affect the angular.io app
|
||||
(currently the `aio/` or `packages/` directories, ignoring spec files).
|
||||
- Optionally, the CI script can check whether the PR can be automatically verified (i.e. if the
|
||||
author of the PR is a member of one of the whitelisted GitHub teams or the PR has the specified
|
||||
"trusted PR" label).
|
||||
- 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 that can be used in case one wants to apply special logic depending on the outcome of the
|
||||
pre-verification. For example:
|
||||
1. One might want to deploy automatically verified PRs only. In that case, the pre-verification
|
||||
helps avoid the wasted overhead associated with uploads that are going to be rejected (e.g.
|
||||
building the artifacts, sending them to the server, running checks on the server, detecting the
|
||||
reasons of deployment failure and whether to fail the build, etc).
|
||||
2. One might want to apply additional logic (e.g. different tests) depending on whether the PR is
|
||||
automatically verified or not).
|
||||
- The CI script gzips and uploads the build artifacts to the server.
|
||||
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 the upload request.
|
||||
- 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 runs several checks to determine whether the request should be accepted and
|
||||
whether it should be publicly accessible or stored for later verification (more details can be
|
||||
- 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 changes the "visibility" of the associated PR, if necessary. For example, if
|
||||
builds for the same PR had been previously deployed as non-public and the current build has been
|
||||
automatically verified, all previous builds are made public as well.
|
||||
If the PR transitions from "non-public" to "public", the upload-server posts a comment on the
|
||||
corresponding PR on GitHub mentioning the SHAs and the links where the previews can be found.
|
||||
- The upload-server verifies that the uploaded file is not trying to overwrite an existing build.
|
||||
- The upload-server deploys the artifacts to a sub-directory named after the PR number and the first
|
||||
few characters of the SHA: `<PR>/<SHA>/`
|
||||
(Non-publicly accessible PRs will be stored in a different location, but again derived from the PR
|
||||
number and SHA.)
|
||||
- If the PR is publicly accessible, the upload-server posts a comment on the corresponding PR on
|
||||
GitHub mentioning the SHA and the link where the preview can be found.
|
||||
|
||||
More info on the possible HTTP status codes and their meaning can be found
|
||||
[here](overview--http-status-codes.md).
|
||||
|
||||
|
||||
### Updating PR visibility
|
||||
- nginx receives a natification that a PR has been updated and passes it through to the
|
||||
upload-server. This could, for example, be sent by a GitHub webhook every time a PR's labels
|
||||
change.
|
||||
E.g.: `ngbuilds.io/pr-updated` (payload: `{"number":<PR>,"action":"labeled"}`)
|
||||
- The request contains the PR number (as `number`) and optionally the action that triggered the
|
||||
request (as `action`) in the payload.
|
||||
- The upload-server verifies the payload and determines whether the `action` (if specified) could
|
||||
have led to PR visibility changes. Only requests that omit the `action` field altogether or
|
||||
specify an action that can affect visibility are further processed.
|
||||
(Currently, the only actions that are considered capable of affecting visibility are `labeled` and
|
||||
`unlabeled`.)
|
||||
- The upload-server re-checks and if necessary updates the PR's visibility.
|
||||
|
||||
More info on the possible HTTP status codes and their meaning can be found
|
||||
[here](overview--http-status-codes.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
|
||||
@ -104,9 +71,6 @@ More info on the possible HTTP status codes and their meaning can be found
|
||||
- nginx maps the subdomain to the correct sub-directory and serves the resource.
|
||||
E.g.: `/<PR>/<SHA>/path/to/resource`
|
||||
|
||||
More info on the possible HTTP status codes and their meaning can be found
|
||||
[here](overview--http-status-codes.md).
|
||||
|
||||
|
||||
### Removing obsolete artifacts
|
||||
In order to avoid flooding the disk with unnecessary build artifacts, there is a cronjob that runs a
|
||||
|
@ -1,85 +0,0 @@
|
||||
# Overview - HTTP Status Codes
|
||||
|
||||
|
||||
This is a list of all the possible HTTP status codes returned by the nginx anf upload servers, along
|
||||
with a bried explanation of what they mean:
|
||||
|
||||
|
||||
## `http://*.ngbuilds.io/*`
|
||||
|
||||
- **307 (Temporary Redirect)**:
|
||||
All non-HTTPS requests. 308 (Permanent Redirect) would be more appropriate, but is not supported
|
||||
by all agents (e.g. cURL).
|
||||
|
||||
|
||||
## `https://pr<pr>-<sha>.ngbuilds.io/*`
|
||||
|
||||
- **200 (OK)**:
|
||||
File was found or URL was rewritten to `/index.html` (i.e. all paths that have no `.` in final
|
||||
segment).
|
||||
|
||||
- **403 (Forbidden)**:
|
||||
Trying to access a sub-directory.
|
||||
|
||||
- **404 (Not Found)**:
|
||||
File not found.
|
||||
|
||||
|
||||
## `https://ngbuilds.io/create-build/<pr>/<sha>`
|
||||
|
||||
- **201 (Created)**:
|
||||
Build deployed successfully and is publicly available.
|
||||
|
||||
- **202 (Accepted)**:
|
||||
Build not automatically verifiable. Stored for later deployment (after re-verification).
|
||||
|
||||
- **400 (Bad Request)**:
|
||||
No payload.
|
||||
|
||||
- **401 (Unauthorized)**:
|
||||
No `AUTHORIZATION` header.
|
||||
|
||||
- **403 (Forbidden)**:
|
||||
Unable to verify build (e.g. invalid JWT token, or unable to talk to 3rd-party APIs, etc).
|
||||
|
||||
- **405 (Method Not Allowed)**:
|
||||
Request method other than POST.
|
||||
|
||||
- **409 (Conflict)**:
|
||||
Request to overwrite existing (public or non-public) directory (e.g. deploy existing build or
|
||||
change PR visibility when the destination directory does already exist).
|
||||
|
||||
- **413 (Payload Too Large)**:
|
||||
Payload larger than size specified in `AIO_UPLOAD_MAX_SIZE`.
|
||||
|
||||
|
||||
## `https://ngbuilds.io/health-check`
|
||||
|
||||
- **200 (OK)**:
|
||||
The server is healthy (i.e. up and running and processing requests).
|
||||
|
||||
|
||||
## `https://ngbuilds.io/pr-updated`
|
||||
|
||||
- **200 (OK)**:
|
||||
Request processed successfully. Processing may or may not have resulted in further actions.
|
||||
|
||||
- **400 (Bad Request)**:
|
||||
No payload or no `number` field in payload.
|
||||
|
||||
- **405 (Method Not Allowed)**:
|
||||
Request method other than POST.
|
||||
|
||||
- **409 (Conflict)**:
|
||||
Request to overwrite existing (public or non-public) directory (i.e. directories for both
|
||||
visibilities exist).
|
||||
(Normally, this should not happen.)
|
||||
|
||||
|
||||
## `https://*.ngbuilds.io/*`
|
||||
|
||||
- **404 (Not Found)**:
|
||||
Request not matched by the above rules.
|
||||
|
||||
- **500 (Internal Server Error)**:
|
||||
Error while processing a request matched by the above rules.
|
@ -8,17 +8,19 @@ This is an overview of the available scripts and commands.
|
||||
The scripts are located inside `<aio-builds-setup-dir>/scripts/`. The following scripts are
|
||||
available:
|
||||
|
||||
- `create-image.sh`:
|
||||
- `build.sh`:
|
||||
Can be used for creating a preconfigured docker image.
|
||||
See [here](vm-setup--create-docker-image.md) for more info.
|
||||
|
||||
- `test.sh`:
|
||||
- `test.sh`
|
||||
Can be used for running the tests for `<aio-builds-setup-dir>/dockerbuild/scripts-js/`. This is
|
||||
useful for CI integration. See [here](misc--integrate-with-ci.md) for more info.
|
||||
|
||||
- `update-preview-server.sh`:
|
||||
Can be used for updating the docker container (and image) based on the latest changes checked out
|
||||
from a git repository. See [here](vm-setup--update-docker-container.md) for more info.
|
||||
- `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
|
||||
|
@ -41,13 +41,12 @@ part of the CI setup and serving them publicly.
|
||||
The implemented approach can be broken up to the following sub-tasks:
|
||||
|
||||
1. Verify which PR the uploaded artifacts correspond to.
|
||||
2. Fetch the PR's metadata, including author and labels.
|
||||
3. Check whether the PR can be automatically verified as "trusted" (based on its author or labels).
|
||||
4. If necessary, update the corresponding PR's verification status.
|
||||
5. Deploy the artifacts to the corresponding PR's directory.
|
||||
6. Prevent overwriting previously deployed artifacts (which ensures that the guarantees established
|
||||
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).
|
||||
7. Prevent uploaded files from accessing anything outside their directory.
|
||||
6. Prevent uploaded files from accessing anything outside their directory.
|
||||
|
||||
|
||||
### Implementation details
|
||||
@ -66,51 +65,35 @@ This section describes how each of the aforementioned sub-tasks is accomplished:
|
||||
_There are currently certain limitation in the implementation of the JWT addon._
|
||||
_See the next section for more details._
|
||||
|
||||
2. **Fetch the PR's metadata, including author and labels**.
|
||||
2. **Determine the author of the PR.**
|
||||
|
||||
Once we have securely associated the uploaded artifacts to a PR, we retrieve the PR's metadata -
|
||||
including the author's username and the labels - using the
|
||||
[GitHub API](https://developer.github.com/v3/).
|
||||
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 can be automatically verified as "trusted"**.
|
||||
3. **Check whether the PR author is a member of some whitelisted GitHub team.**
|
||||
|
||||
"Trusted" means that we are confident that the build artifacts are suitable for being deployed
|
||||
and publicly accessible on the preview server. There are two ways to check that:
|
||||
1. We can verify that the PR has a pre-determined label, which marks it as "safe for preview".
|
||||
Such a label can only have been added by a maintainer (with the necessary rights) and
|
||||
designates that they have manually verified the PR contents.
|
||||
2. We can verify (again using the GitHub API) the author's membership in one of the
|
||||
whitelisted/trusted GitHub teams. For this operation, we need a Personal Access Token with the
|
||||
`read:org` scope issued by a user that can "see" the specified GitHub organization.
|
||||
Here too, we use the token by @mary-poppins.
|
||||
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. **If necessary update the corresponding PR's verification status**.
|
||||
4. **Deploy the artifacts to the corresponding PR's directory.**
|
||||
|
||||
Once we have determined whether the PR is considered "trusted", we update its "visibility" (i.e.
|
||||
whether it is publicly accessible or not), based on the new verification status. For example, if
|
||||
a PR was initially considered "not trusted" but the check triggered by a new build determined
|
||||
otherwise, the PR (and all the previously uploaded previews) are made public. It works the same
|
||||
way if a PR has gone from "trusted" to "not trusted".
|
||||
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. **Deploy the artifacts to the corresponding PR's directory.**
|
||||
5. **Prevent overwriting previously deployed artifacts**.
|
||||
|
||||
With the preceding steps, we have verified that the uploaded artifacts have been uploaded by
|
||||
Travis. Additionally, we have determined whether the PR can be trusted to have its previews
|
||||
publicly accessible or whether further verification is necessary. The artifacts will be stored to
|
||||
the PR's directory, but will not be publicly accessible unless the PR has been verified.
|
||||
Essentially, as long as sub-tasks 1, 2 and 3 can be securely accomplished, it is possible to
|
||||
"project" the trust we have in a team's members through the PR and Travis to the build artifacts.
|
||||
|
||||
6. **Prevent overwriting previously deployed artifacts**.
|
||||
|
||||
In order to enforce this restriction (and ensure that the deployed artifacts' validity is
|
||||
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._
|
||||
|
||||
7. **Prevent uploaded files from accessing anything outside their directory.**
|
||||
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.
|
||||
@ -121,11 +104,6 @@ This section describes how each of the aforementioned sub-tasks is accomplished:
|
||||
- Each trusted PR author has full control over the content that is uploaded for their PRs. Part of
|
||||
the security model relies on the trustworthiness of these authors.
|
||||
|
||||
- Adding the specified label on a PR and marking it as trusted, gives the author full control over
|
||||
the content that is uploaded for the specific PR (e.g. by pushing more commits to it). The user
|
||||
adding the label is responsible for ensuring that this control is not abused and that the PR is
|
||||
either closed (one way of another) or the access is revoked.
|
||||
|
||||
- If anyone gets access to the `PREVIEW_DEPLOYMENT_TOKEN` (a.k.a. `NGBUILDS_IO_KEY` on
|
||||
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
|
||||
|
@ -6,11 +6,11 @@
|
||||
|
||||
|
||||
## Build docker image
|
||||
- `<aio-builds-setup-dir>/scripts/create-image.sh [<name>[:<tag>] [--build-arg <NAME>=<value> ...]]`
|
||||
- `<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 script has to execute docker commands with `sudo`.
|
||||
**Note:** The build script has to execute docker commands with `sudo`.
|
||||
|
||||
|
||||
## Example
|
||||
@ -25,7 +25,7 @@ The following commands would create a docker image from GitHub repo `foo/bar` to
|
||||
--build-arg AIO_REPO_SLUG=foo/bar \
|
||||
--build-arg AIO_DOMAIN_NAME=foobar-builds.io \
|
||||
--build-arg AIO_GITHUB_ORGANIZATION=foo \
|
||||
--build-arg AIO_GITHUB_TEAM_SLUGS=bar-core,bar-docs-authors
|
||||
--build-arg AIO_GITHUB_TEMA_SLUGS=bar-core,bar-docs-authors
|
||||
```
|
||||
|
||||
A full list of the available environment variables can be found
|
||||
|
@ -9,7 +9,7 @@ Necessary secrets:
|
||||
- Used for:
|
||||
- Retrieving open PRs without rate-limiting.
|
||||
- Retrieving PR author.
|
||||
- Retrieving members of the trusted GitHub teams.
|
||||
- Retrieving members of the `angular-core` team.
|
||||
- Posting comments with preview links on PRs.
|
||||
|
||||
2. `PREVIEW_DEPLOYMENT_TOKEN`
|
||||
|
@ -7,16 +7,16 @@ command:
|
||||
|
||||
```
|
||||
sudo docker run \
|
||||
--detach \
|
||||
-d \
|
||||
--dns 127.0.0.1 \
|
||||
--name <instance-name> \
|
||||
--publish 80:80 \
|
||||
--publish 443:443 \
|
||||
-p 80:80 \
|
||||
-p 443:443 \
|
||||
--restart unless-stopped \
|
||||
[--volume <host-cert-dir>:/etc/ssl/localcerts:ro] \
|
||||
--volume <host-secrets-dir>:/aio-secrets:ro \
|
||||
--volume <host-builds-dir>:/var/www/aio-builds \
|
||||
[--volume <host-logs-dir>:/var/log/aio] \
|
||||
[-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>]
|
||||
```
|
||||
|
||||
@ -27,7 +27,7 @@ can be found [here](https://docs.docker.com/engine/reference/run/).
|
||||
sudo docker run \
|
||||
|
||||
# Start as a daemon.
|
||||
--detach \
|
||||
-d \
|
||||
|
||||
# Use the local DNS server.
|
||||
# (This is necessary for mapping internal URLs, e.g. for the Node.js upload-server.)
|
||||
@ -37,9 +37,9 @@ sudo docker run \
|
||||
# Useful for running `docker` commands, e.g.: `docker stop <instance-name>`
|
||||
--name <instance-name> \
|
||||
|
||||
# Map ports of the host VM (left) to ports of the docker container (right)
|
||||
--publish 80:80 \
|
||||
--publish 443:443 \
|
||||
# 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.)
|
||||
@ -48,22 +48,22 @@ sudo docker run \
|
||||
# The directory the contains the SSL certificates.
|
||||
# (See [here](vm-setup--create-host-dirs-and-files.md) for more info.)
|
||||
# If not provided, the container will use self-signed certificates.
|
||||
[--volume <host-cert-dir>:/etc/ssl/localcerts:ro] \
|
||||
[-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.)
|
||||
--volume <host-secrets-dir>:/aio-secrets:ro \
|
||||
-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.)
|
||||
--volume <host-builds-dir>:/var/www/aio-builds \
|
||||
-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.
|
||||
[--volume <host-logs-dir>:/var/log/aio] \
|
||||
[-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.)
|
||||
@ -74,19 +74,19 @@ sudo docker run \
|
||||
## 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 accessing secrets and SSL certificates and keeping the build artifacts and logs.
|
||||
by the container for accesing secrets and SSL certificates and keeping the build artifacts and logs.
|
||||
|
||||
```
|
||||
sudo docker run \
|
||||
--detach \
|
||||
-d \
|
||||
--dns 127.0.0.1 \
|
||||
--name foobar-builds-1 \
|
||||
--publish 80:80 \
|
||||
--publish 443:443 \
|
||||
-p 80:80 \
|
||||
-p 443:443 \
|
||||
--restart unless-stopped \
|
||||
--volume /etc/ssl/localcerts:/etc/ssl/localcerts:ro \
|
||||
--volume /foobar-secrets:/aio-secrets:ro \
|
||||
--volume /mnt/disks/foobar-builds:/var/www/aio-builds \
|
||||
--volume /foobar-logs:/var/log/aio \
|
||||
-v /etc/ssl/localcerts:/etc/ssl/localcerts:ro \
|
||||
-v /foobar-secrets:/aio-secrets:ro \
|
||||
-v /mnt/disks/foobar-builds:/var/www/aio-builds \
|
||||
-v /foobar-logs:/var/log/aio \
|
||||
foobar-builds
|
||||
```
|
||||
|
@ -1,52 +0,0 @@
|
||||
# VM setup - Update docker container
|
||||
|
||||
|
||||
## Overview
|
||||
Assuming you have cloned the repository containing the preview server code (as described
|
||||
[here](vm-setup--create-docker-image.md)), you can use the `update-preview-server.sh` script on the
|
||||
VM host to update the preview server based on changes in the source code.
|
||||
|
||||
The script will pull the latest changes from the origin's master branch and examine if there have
|
||||
been any changes in files inside the preview server source code directory (see below). If there are,
|
||||
it will create a new image and verify that is works as expected. Finally, it will stop and remove
|
||||
the old docker container and image, create a new container based on the new image and start it.
|
||||
|
||||
The script assumes that the preview server source code is in the repository's
|
||||
`aio/aio-builds-setup/` directory and expects the following inputs:
|
||||
|
||||
- **$1**: `HOST_REPO_DIR`
|
||||
- **$2**: `HOST_LOCALCERTS_DIR`
|
||||
- **$3**: `HOST_SECRETS_DIR`
|
||||
- **$4**: `HOST_BUILDS_DIR`
|
||||
- **$5**: `HOST_LOGS_DIR`
|
||||
|
||||
See [here](vm-setup--create-host-dirs-and-files.md) for more info on what each input directory is
|
||||
used for.
|
||||
|
||||
**Note 1:** The script has to execute docker commands with `sudo`.
|
||||
|
||||
**Note 2:** Make sure the user that executes the script has access to update the repository
|
||||
|
||||
|
||||
## Run the script manually
|
||||
You may choose to manually run the script, when necessary. Example:
|
||||
|
||||
```
|
||||
update-preview-server.sh \
|
||||
/path/to/repo \
|
||||
/path/to/localcerts \
|
||||
/path/to/secrets \
|
||||
/path/to/builds \
|
||||
/path/to/logs
|
||||
```
|
||||
|
||||
|
||||
## Run the script automatically
|
||||
You may choose to automatically trigger the script, e.g. using a cronjob. For example, the following
|
||||
cronjob entry would run the script every hour and update the preview server (assuming the user has
|
||||
the necessary permissions):
|
||||
|
||||
```
|
||||
# Periodically check for changes and update the preview server (if necessary)
|
||||
*/30 * * * * /path/to/update-preview-server.sh /path/to/repo /path/to/localcerts /path/to/secrets /path/to/builds /path/to/logs
|
||||
```
|
16
aio/aio-builds-setup/scripts/build.sh
Executable file
16
aio/aio-builds-setup/scripts/build.sh
Executable file
@ -0,0 +1,16 @@
|
||||
#!/bin/bash
|
||||
set -eux -o pipefail
|
||||
|
||||
# Set up env
|
||||
source "`dirname $0`/env.sh"
|
||||
readonly defaultImageNameAndTag="aio-builds:latest"
|
||||
|
||||
# Build `scripts-js/`
|
||||
cd "$SCRIPTS_JS_DIR"
|
||||
yarn install
|
||||
yarn run build
|
||||
cd -
|
||||
|
||||
# Create docker image
|
||||
readonly nameAndOptionalTag=${1:-$defaultImageNameAndTag}
|
||||
sudo docker build --tag $nameAndOptionalTag ${@:2} $DOCKERBUILD_DIR
|
@ -1,18 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -eux -o pipefail
|
||||
|
||||
# Set up env
|
||||
source "`dirname $0`/_env.sh"
|
||||
readonly defaultImageNameAndTag="aio-builds:latest"
|
||||
|
||||
# Build `scripts-js/`
|
||||
# (Necessary, because only `scripts-js/dist/` is copied to the docker image.)
|
||||
(
|
||||
cd "$SCRIPTS_JS_DIR"
|
||||
yarn install --frozen-lockfile --non-interactive
|
||||
yarn build
|
||||
)
|
||||
|
||||
# Create docker image
|
||||
readonly nameAndOptionalTag=${1:-$defaultImageNameAndTag}
|
||||
sudo docker build --tag $nameAndOptionalTag ${@:2} $DOCKERBUILD_DIR
|
@ -2,11 +2,10 @@
|
||||
set -eux -o pipefail
|
||||
|
||||
# Set up env
|
||||
source "`dirname $0`/_env.sh"
|
||||
source "`dirname $0`/env.sh"
|
||||
|
||||
# Test `scripts-js/`
|
||||
(
|
||||
cd "$SCRIPTS_JS_DIR"
|
||||
yarn install --frozen-lockfile --non-interactive
|
||||
yarn test
|
||||
)
|
||||
cd "$SCRIPTS_JS_DIR"
|
||||
yarn install
|
||||
yarn test
|
||||
cd -
|
||||
|
13
aio/aio-builds-setup/scripts/travis-preverify-pr.sh
Executable file
13
aio/aio-builds-setup/scripts/travis-preverify-pr.sh
Executable file
@ -0,0 +1,13 @@
|
||||
#!/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"
|
@ -1,70 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -eux -o pipefail
|
||||
exec 3>&1
|
||||
|
||||
echo -e "\n\n[`date`] - Updating the preview server..."
|
||||
|
||||
# Input
|
||||
readonly HOST_REPO_DIR=$1
|
||||
readonly HOST_LOCALCERTS_DIR=$2
|
||||
readonly HOST_SECRETS_DIR=$3
|
||||
readonly HOST_BUILDS_DIR=$4
|
||||
readonly HOST_LOGS_DIR=$5
|
||||
|
||||
# Constants
|
||||
readonly PROVISIONAL_IMAGE_NAME=aio-builds:provisional
|
||||
readonly LATEST_IMAGE_NAME=aio-builds:latest
|
||||
readonly CONTAINER_NAME=aio
|
||||
|
||||
# Run
|
||||
(
|
||||
cd "$HOST_REPO_DIR"
|
||||
|
||||
readonly lastDeployedCommit=$(git rev-parse HEAD)
|
||||
echo "Currently at commit $lastDeployedCommit."
|
||||
|
||||
# Pull latest master from origin.
|
||||
git pull origin master
|
||||
|
||||
# Do not update the server unless files inside `aio-builds-setup/` have changed
|
||||
# or the last attempt failed (identified by the provisional image still being around).
|
||||
readonly relevantChangedFilesCount=$(git diff --name-only $lastDeployedCommit...HEAD | grep -P "^aio/aio-builds-setup/" | wc -l)
|
||||
readonly lastAttemptFailed=$(sudo docker rmi "$PROVISIONAL_IMAGE_NAME" >> /dev/fd/3 && echo "true" || echo "false")
|
||||
if [[ $relevantChangedFilesCount -eq 0 ]] && [[ "$lastAttemptFailed" != "true" ]]; then
|
||||
echo "Skipping update because no relevant files have been touched."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Create and verify a new docker image.
|
||||
aio/aio-builds-setup/scripts/create-image.sh "$PROVISIONAL_IMAGE_NAME" --no-cache
|
||||
readonly imageVerified=$(sudo docker run --dns 127.0.0.1 --rm --volume $HOST_SECRETS_DIR:/aio-secrets:ro "$PROVISIONAL_IMAGE_NAME" /bin/bash -c "aio-init && aio-health-check && aio-verify-setup" >> /dev/fd/3 && echo "true" || echo "false")
|
||||
|
||||
if [[ "$imageVerified" != "true" ]]; then
|
||||
echo "Failed to verify new docker image. Aborting update!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Remove the old container and replace the docker image.
|
||||
sudo docker stop "$CONTAINER_NAME" || true
|
||||
sudo docker rm "$CONTAINER_NAME" || true
|
||||
sudo docker rmi "$LATEST_IMAGE_NAME" || true
|
||||
sudo docker tag "$PROVISIONAL_IMAGE_NAME" "$LATEST_IMAGE_NAME"
|
||||
sudo docker rmi "$PROVISIONAL_IMAGE_NAME"
|
||||
|
||||
# Create and start a docker container based on the new image.
|
||||
sudo docker run \
|
||||
--detach \
|
||||
--dns 127.0.0.1 \
|
||||
--name "$CONTAINER_NAME" \
|
||||
--publish 80:80 \
|
||||
--publish 443:443 \
|
||||
--restart unless-stopped \
|
||||
--volume $HOST_LOCALCERTS_DIR:/etc/ssl/localcerts:ro \
|
||||
--volume $HOST_SECRETS_DIR:/aio-secrets:ro \
|
||||
--volume $HOST_BUILDS_DIR:/var/www/aio-builds \
|
||||
--volume $HOST_LOGS_DIR:/var/log/aio \
|
||||
"$LATEST_IMAGE_NAME"
|
||||
|
||||
echo "The new docker image has been successfully deployed."
|
||||
)
|
189
aio/angular.json
189
aio/angular.json
@ -1,189 +0,0 @@
|
||||
{
|
||||
"$schema": "./node_modules/@angular-devkit/core/src/workspace/workspace-schema.json",
|
||||
"version": 1,
|
||||
"cli": {
|
||||
"packageManager": "yarn"
|
||||
},
|
||||
"newProjectRoot": "projects",
|
||||
"projects": {
|
||||
"site": {
|
||||
"root": "",
|
||||
"sourceRoot": "src",
|
||||
"projectType": "application",
|
||||
"architect": {
|
||||
"build": {
|
||||
"builder": "@angular-devkit/build-angular:browser",
|
||||
"options": {
|
||||
"outputPath": "dist",
|
||||
"index": "src/index.html",
|
||||
"main": "src/main.ts",
|
||||
"tsConfig": "src/tsconfig.app.json",
|
||||
"aot": true,
|
||||
"optimization": true,
|
||||
"buildOptimizer": true,
|
||||
"outputHashing": "all",
|
||||
"sourceMap": true,
|
||||
"statsJson": true,
|
||||
"extractCss": true,
|
||||
"extractLicenses": true,
|
||||
"namedChunks": true,
|
||||
"vendorChunk": false,
|
||||
"polyfills": "src/polyfills.ts",
|
||||
"assets": [
|
||||
"src/assets",
|
||||
"src/generated",
|
||||
"src/app/search/search-worker.js",
|
||||
"src/favicon.ico",
|
||||
"src/pwa-manifest.json",
|
||||
"src/google385281288605d160.html",
|
||||
{
|
||||
"glob": "custom-elements.min.js",
|
||||
"input": "node_modules/@webcomponents/custom-elements",
|
||||
"output": "/assets/js"
|
||||
},
|
||||
{
|
||||
"glob": "native-shim.js",
|
||||
"input": "node_modules/@webcomponents/custom-elements/src",
|
||||
"output": "/assets/js"
|
||||
}
|
||||
],
|
||||
"styles": [
|
||||
"src/styles.scss"
|
||||
],
|
||||
"scripts": []
|
||||
},
|
||||
"configurations": {
|
||||
"fast": {
|
||||
"optimization": false
|
||||
},
|
||||
"next": {
|
||||
"fileReplacements": [
|
||||
{
|
||||
"src": "src/environments/environment.ts",
|
||||
"replaceWith": "src/environments/environment.next.ts"
|
||||
}
|
||||
]
|
||||
},
|
||||
"stable": {
|
||||
"fileReplacements": [
|
||||
{
|
||||
"src": "src/environments/environment.ts",
|
||||
"replaceWith": "src/environments/environment.stable.ts"
|
||||
}
|
||||
]
|
||||
},
|
||||
"archive": {
|
||||
"fileReplacements": [
|
||||
{
|
||||
"src": "src/environments/environment.ts",
|
||||
"replaceWith": "src/environments/environment.archive.ts"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"serve": {
|
||||
"builder": "@angular-devkit/build-angular:dev-server",
|
||||
"options": {
|
||||
"browserTarget": "site:build"
|
||||
},
|
||||
"configurations": {
|
||||
"fast": {
|
||||
"browserTarget": "site:build:fast"
|
||||
},
|
||||
"next": {
|
||||
"browserTarget": "site:build:next"
|
||||
},
|
||||
"stable": {
|
||||
"browserTarget": "site:build:stable"
|
||||
},
|
||||
"archive": {
|
||||
"browserTarget": "site:build:archive"
|
||||
}
|
||||
}
|
||||
},
|
||||
"extract-i18n": {
|
||||
"builder": "@angular-devkit/build-angular:extract-i18n",
|
||||
"options": {
|
||||
"browserTarget": "site:build"
|
||||
}
|
||||
},
|
||||
"test": {
|
||||
"builder": "@angular-devkit/build-angular:karma",
|
||||
"options": {
|
||||
"main": "src/test.ts",
|
||||
"karmaConfig": "src/karma.conf.js",
|
||||
"polyfills": "src/polyfills.ts",
|
||||
"tsConfig": "src/tsconfig.spec.json",
|
||||
"scripts": [],
|
||||
"styles": [
|
||||
"src/styles.scss"
|
||||
],
|
||||
"assets": [
|
||||
"src/assets",
|
||||
"src/generated",
|
||||
"src/app/search/search-worker.js",
|
||||
"src/favicon.ico",
|
||||
"src/pwa-manifest.json",
|
||||
"src/google385281288605d160.html",
|
||||
{
|
||||
"glob": "custom-elements.min.js",
|
||||
"input": "node_modules/@webcomponents/custom-elements",
|
||||
"output": "/assets/js"
|
||||
},
|
||||
{
|
||||
"glob": "native-shim.js",
|
||||
"input": "node_modules/@webcomponents/custom-elements/src",
|
||||
"output": "/assets/js"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"lint": {
|
||||
"builder": "@angular-devkit/build-angular:tslint",
|
||||
"options": {
|
||||
"tsConfig": [
|
||||
"src/tsconfig.app.json",
|
||||
"src/tsconfig.spec.json"
|
||||
],
|
||||
"exclude": []
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"site-e2e": {
|
||||
"root": "",
|
||||
"projectType": "application",
|
||||
"cli": {},
|
||||
"schematics": {},
|
||||
"architect": {
|
||||
"e2e": {
|
||||
"builder": "@angular-devkit/build-angular:protractor",
|
||||
"options": {
|
||||
"protractorConfig": "tests/e2e/protractor.conf.js",
|
||||
"devServerTarget": "site:serve"
|
||||
}
|
||||
},
|
||||
"lint": {
|
||||
"builder": "@angular-devkit/build-angular:tslint",
|
||||
"options": {
|
||||
"tsConfig": [
|
||||
"tests/e2e/tsconfig.e2e.json"
|
||||
],
|
||||
"exclude": []
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"schematics": {
|
||||
"@schematics/angular:component": {
|
||||
"inlineStyle": true,
|
||||
"prefix": "aio",
|
||||
"styleext": "scss"
|
||||
},
|
||||
"@schematics/angular:directive": {
|
||||
"prefix": "aio"
|
||||
}
|
||||
}
|
||||
}
|
50
aio/content/examples/.gitignore
vendored
50
aio/content/examples/.gitignore
vendored
@ -1,38 +1,25 @@
|
||||
# boilerplate files
|
||||
**/src/environments/environment.prod.ts
|
||||
**/src/environments/environment.ts
|
||||
**/src/assets/.gitkeep
|
||||
**/src/favicon.ico
|
||||
**/src/styles.css
|
||||
**/src/systemjs-angular-loader.js
|
||||
**/src/systemjs.config.js
|
||||
**/src/tsconfig.json
|
||||
**/src/favicon.ico
|
||||
**/src/polyfills.ts
|
||||
**/src/test.ts
|
||||
**/src/tsconfig.app.json
|
||||
**/src/tsconfig.spec.json
|
||||
**/src/typings.d.ts
|
||||
**/e2e/src/app.po.ts
|
||||
**/e2e/tsconfig.e2e.json
|
||||
**/src/karma.conf.js
|
||||
**/.angular-cli.json
|
||||
**/.editorconfig
|
||||
**/angular.json
|
||||
**/tsconfig.json
|
||||
**/bs-config.e2e.json
|
||||
**/bs-config.json
|
||||
**/package.json
|
||||
**/tslint.json
|
||||
**/karma.conf.js
|
||||
**/karma-test-shim.js
|
||||
**/browser-test-shim.js
|
||||
**/browserslist
|
||||
**/node_modules
|
||||
|
||||
# built files
|
||||
*.map
|
||||
_test-output
|
||||
protractor-helpers.js
|
||||
*/e2e-spec.js
|
||||
**/*.js
|
||||
**/ts/**/*.js
|
||||
**/js-es6*/**/*.js
|
||||
dist/
|
||||
|
||||
|
||||
@ -56,31 +43,28 @@ dist/
|
||||
**/app/**/*.ajs.js
|
||||
|
||||
# aot
|
||||
**/*.ngfactory.ts
|
||||
**/*.ngsummary.json
|
||||
**/*.shim.ngstyle.ts
|
||||
**/*.metadata.json
|
||||
!aot/bs-config.json
|
||||
!aot/index.html
|
||||
!rollup-config.js
|
||||
aot-compiler/**/*.d.ts
|
||||
aot-compiler/**/*.factory.d.ts
|
||||
|
||||
# i18n
|
||||
!i18n/src/systemjs-text-plugin.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
|
||||
!webpack/src/polyfills.ts
|
||||
|
||||
# styleguide
|
||||
!styleguide/src/systemjs.custom.js
|
||||
|
||||
# universal
|
||||
!universal/webpack.server.config.js
|
||||
|
||||
# stackblitz
|
||||
*stackblitz.no-link.html
|
||||
|
||||
# ngUpgrade testing
|
||||
!upgrade-phonecat-*/**/karma.conf.js
|
||||
!upgrade-phonecat-*/**/karma-test-shim.js
|
||||
# plunkers
|
||||
*plnkr.no-link.html
|
||||
|
@ -1,62 +0,0 @@
|
||||
{
|
||||
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
|
||||
"project": {
|
||||
"name": "angular.io-example"
|
||||
},
|
||||
"apps": [
|
||||
{
|
||||
"root": "src",
|
||||
"outDir": "dist",
|
||||
"assets": [
|
||||
"assets",
|
||||
"favicon.ico"
|
||||
],
|
||||
"index": "index.html",
|
||||
"main": "main.ts",
|
||||
"polyfills": "polyfills.ts",
|
||||
"test": "test.ts",
|
||||
"tsconfig": "tsconfig.app.json",
|
||||
"testTsconfig": "tsconfig.spec.json",
|
||||
"prefix": "app",
|
||||
// #docregion styles
|
||||
"styles": [
|
||||
"styles.css"
|
||||
],
|
||||
// #enddocregion styles
|
||||
"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",
|
||||
"exclude": "**/node_modules/**"
|
||||
},
|
||||
{
|
||||
"project": "src/tsconfig.spec.json",
|
||||
"exclude": "**/node_modules/**"
|
||||
},
|
||||
{
|
||||
"project": "e2e/tsconfig.e2e.json",
|
||||
"exclude": "**/node_modules/**"
|
||||
}
|
||||
],
|
||||
"test": {
|
||||
"karma": {
|
||||
"config": "./karma.conf.js"
|
||||
}
|
||||
},
|
||||
"defaults": {
|
||||
"styleExt": "css",
|
||||
"component": {}
|
||||
}
|
||||
}
|
115
aio/content/examples/ajs-quick-reference/e2e-spec.ts
Normal file
115
aio/content/examples/ajs-quick-reference/e2e-spec.ts
Normal file
@ -0,0 +1,115 @@
|
||||
'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);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
@ -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('app-movie-list tr > th > button'));
|
||||
expect(posterButton.getText()).toBe(expectedButtonText);
|
||||
|
||||
posterButton.click().then(function () {
|
||||
testImagesAreDisplayed(isDisplayed);
|
||||
});
|
||||
}
|
||||
|
||||
function getMovieRows() {
|
||||
return element.all(by.css('app-movie-list tbody > tr'));
|
||||
}
|
||||
|
||||
function testFavoriteHero(heroName: string, expectedLabel: string) {
|
||||
let movieListComp = element(by.tagName('app-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);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
10
aio/content/examples/ajs-quick-reference/plnkr.json
Normal file
10
aio/content/examples/ajs-quick-reference/plnkr.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"description": "AngularJS to Angular Quick Reference",
|
||||
"basePath": "src/",
|
||||
"files":[
|
||||
"!**/*.d.ts",
|
||||
"!**/*.js",
|
||||
"!**/*.[1].*"
|
||||
],
|
||||
"tags":["cookbook", "angularjs"]
|
||||
}
|
@ -14,13 +14,13 @@
|
||||
<h1>Example Snippets</h1>
|
||||
|
||||
<!-- #docregion ngClass -->
|
||||
<div [ngClass]="{'active': isActive}">
|
||||
<div [ngClass]="{active: isActive}">
|
||||
<!-- #enddocregion ngClass -->
|
||||
[ngClass] active
|
||||
</div>
|
||||
<!-- #docregion ngClass -->
|
||||
<div [ngClass]="{'active': isActive,
|
||||
'shazam': isImportant}">
|
||||
<div [ngClass]="{active: isActive,
|
||||
shazam: isImportant}">
|
||||
<!-- #enddocregion ngClass -->
|
||||
[ngClass] active and boldly important
|
||||
</div>
|
||||
@ -57,7 +57,7 @@
|
||||
|
||||
<p></p>
|
||||
<!-- #docregion ngStyle -->
|
||||
<div [ngStyle]="{'color': colorPreference}">
|
||||
<div [ngStyle]="{color: colorPreference}">
|
||||
<!-- #enddocregion ngStyle -->
|
||||
color preference #1
|
||||
</div>
|
||||
|
@ -4,7 +4,7 @@ import { MovieService } from './movie.service';
|
||||
import { IMovie } from './movie';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
selector: 'my-app',
|
||||
templateUrl: './app.component.html',
|
||||
styleUrls: [ './app.component.css' ],
|
||||
providers: [ MovieService ]
|
||||
@ -19,7 +19,7 @@ export class AppComponent {
|
||||
movie: IMovie = null;
|
||||
movies: IMovie[] = [];
|
||||
showImage = true;
|
||||
title = 'AngularJS to Angular Quick Ref Cookbook';
|
||||
title: string = 'AngularJS to Angular Quick Ref Cookbook';
|
||||
toggleImage(event: UIEvent) {
|
||||
this.showImage = !this.showImage;
|
||||
this.eventType = (event && event.type) || 'not provided';
|
||||
|
@ -8,7 +8,7 @@ import { MovieService } from './movie.service';
|
||||
|
||||
// #docregion component
|
||||
@Component({
|
||||
selector: 'app-movie-list',
|
||||
selector: 'movie-list',
|
||||
templateUrl: './movie-list.component.html',
|
||||
// #docregion style-url
|
||||
styleUrls: [ './movie-list.component.css' ],
|
||||
@ -19,7 +19,7 @@ import { MovieService } from './movie.service';
|
||||
export class MovieListComponent {
|
||||
// #enddocregion class
|
||||
favoriteHero: string;
|
||||
showImage = false;
|
||||
showImage: boolean = false;
|
||||
movies: IMovie[];
|
||||
|
||||
// #docregion di
|
||||
|
@ -8,7 +8,7 @@ export class MovieService {
|
||||
return [
|
||||
{
|
||||
hero: 'Celeritas',
|
||||
imageurl: 'assets/images/hero.png',
|
||||
imageurl: 'images/hero.png',
|
||||
movieId: 1,
|
||||
mpaa: 'pg-13',
|
||||
releaseDate: '2015-12-19T00:00:00',
|
||||
@ -19,7 +19,7 @@ export class MovieService {
|
||||
},
|
||||
{
|
||||
hero: 'Mr. Nice',
|
||||
imageurl: 'assets/images/villain.png',
|
||||
imageurl: 'images/villain.png',
|
||||
movieId: 2,
|
||||
mpaa: 'pg-13',
|
||||
releaseDate: '2015-12-18T00:00:00',
|
||||
@ -30,7 +30,7 @@ export class MovieService {
|
||||
},
|
||||
{
|
||||
hero: 'Angular',
|
||||
imageurl: 'assets/images/ng-logo.png',
|
||||
imageurl: 'images/ng-logo.png',
|
||||
movieId: 3,
|
||||
mpaa: 'pg-13',
|
||||
releaseDate: '2015-12-17T00:00:00',
|
||||
|
Before Width: | Height: | Size: 7.1 KiB After Width: | Height: | Size: 7.1 KiB |
Before Width: | Height: | Size: 7.1 KiB After Width: | Height: | Size: 7.1 KiB |
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
@ -1,14 +1,28 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<html>
|
||||
<head>
|
||||
<base href="/">
|
||||
<meta charset="UTF-8">
|
||||
<title>AngularJS to Angular Quick Reference</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<!-- #docregion style -->
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
<!-- #enddocregion style -->
|
||||
|
||||
<!-- Polyfills -->
|
||||
<script src="node_modules/core-js/client/shim.min.js"></script>
|
||||
|
||||
<script src="node_modules/zone.js/dist/zone.js"></script>
|
||||
<script src="node_modules/systemjs/dist/system.src.js"></script>
|
||||
|
||||
<script src="systemjs.config.js"></script>
|
||||
<script>
|
||||
System.import('main.js').catch(function(err){ console.error(err); });
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<app-root></app-root>
|
||||
<my-app>Loading app...</my-app>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
@ -1,12 +1,5 @@
|
||||
// #docregion
|
||||
import { enableProdMode } from '@angular/core';
|
||||
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
|
||||
|
||||
import { AppModule } from './app/app.module';
|
||||
import { environment } from './environments/environment';
|
||||
|
||||
if (environment.production) {
|
||||
enableProdMode();
|
||||
}
|
||||
|
||||
platformBrowserDynamic().bootstrapModule(AppModule);
|
||||
|
@ -1,9 +0,0 @@
|
||||
{
|
||||
"description": "AngularJS to Angular Quick Reference",
|
||||
"files":[
|
||||
"!**/*.d.ts",
|
||||
"!**/*.js",
|
||||
"!**/*.[1].*"
|
||||
],
|
||||
"tags":["cookbook", "angularjs"]
|
||||
}
|
1
aio/content/examples/animations/.gitignore
vendored
Normal file
1
aio/content/examples/animations/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
**/*.js
|
351
aio/content/examples/animations/e2e-spec.ts
Normal file
351
aio/content/examples/animations/e2e-spec.ts
Normal file
@ -0,0 +1,351 @@
|
||||
'use strict'; // necessary for es6 output in node
|
||||
|
||||
import { browser, element, by, ElementFinder } from 'protractor';
|
||||
import { logging, promise } from 'selenium-webdriver';
|
||||
|
||||
/**
|
||||
* The tests here basically just checking that the end styles
|
||||
* of each animation are in effect.
|
||||
*
|
||||
* Relies on the Angular testability only becoming stable once
|
||||
* animation(s) have finished.
|
||||
*
|
||||
* Ideally we'd use https://developer.mozilla.org/en-US/docs/Web/API/Document/getAnimations
|
||||
* but they're not supported in Chrome at the moment. The upcoming nganimate polyfill
|
||||
* may also add some introspection support.
|
||||
*/
|
||||
describe('Animation Tests', () => {
|
||||
|
||||
const INACTIVE_COLOR = 'rgba(238, 238, 238, 1)';
|
||||
const ACTIVE_COLOR = 'rgba(207, 216, 220, 1)';
|
||||
const NO_TRANSFORM_MATRIX_REGEX = /matrix\(1,\s*0,\s*0,\s*1,\s*0,\s*0\)/;
|
||||
|
||||
beforeEach(() => {
|
||||
browser.get('');
|
||||
});
|
||||
|
||||
describe('basic states', () => {
|
||||
|
||||
let host: ElementFinder;
|
||||
|
||||
beforeEach(() => {
|
||||
host = element(by.css('hero-list-basic'));
|
||||
});
|
||||
|
||||
it('animates between active and inactive', () => {
|
||||
addInactiveHero();
|
||||
|
||||
let li = host.element(by.css('li'));
|
||||
|
||||
expect(getScaleX(li)).toBe(1.0);
|
||||
expect(li.getCssValue('backgroundColor')).toBe(INACTIVE_COLOR);
|
||||
|
||||
li.click();
|
||||
browser.driver.sleep(300);
|
||||
expect(getScaleX(li)).toBe(1.1);
|
||||
expect(li.getCssValue('backgroundColor')).toBe(ACTIVE_COLOR);
|
||||
|
||||
li.click();
|
||||
browser.driver.sleep(300);
|
||||
expect(getScaleX(li)).toBe(1.0);
|
||||
expect(li.getCssValue('backgroundColor')).toBe(INACTIVE_COLOR);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('styles inline in transitions', () => {
|
||||
|
||||
let host: ElementFinder;
|
||||
|
||||
beforeEach(function() {
|
||||
host = element(by.css('hero-list-inline-styles'));
|
||||
});
|
||||
|
||||
it('are not kept after animation', () => {
|
||||
addInactiveHero();
|
||||
|
||||
let li = host.element(by.css('li'));
|
||||
|
||||
li.click();
|
||||
browser.driver.sleep(300);
|
||||
expect(getScaleX(li)).toBe(1.0);
|
||||
expect(li.getCssValue('backgroundColor')).toBe(INACTIVE_COLOR);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('combined transition syntax', () => {
|
||||
|
||||
let host: ElementFinder;
|
||||
|
||||
beforeEach(() => {
|
||||
host = element(by.css('hero-list-combined-transitions'));
|
||||
});
|
||||
|
||||
it('animates between active and inactive', () => {
|
||||
addInactiveHero();
|
||||
|
||||
let li = host.element(by.css('li'));
|
||||
|
||||
expect(getScaleX(li)).toBe(1.0);
|
||||
expect(li.getCssValue('backgroundColor')).toBe(INACTIVE_COLOR);
|
||||
|
||||
li.click();
|
||||
browser.driver.sleep(300);
|
||||
expect(getScaleX(li)).toBe(1.1);
|
||||
expect(li.getCssValue('backgroundColor')).toBe(ACTIVE_COLOR);
|
||||
|
||||
li.click();
|
||||
browser.driver.sleep(300);
|
||||
expect(getScaleX(li)).toBe(1.0);
|
||||
expect(li.getCssValue('backgroundColor')).toBe(INACTIVE_COLOR);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('two-way transition syntax', () => {
|
||||
|
||||
let host: ElementFinder;
|
||||
|
||||
beforeEach(() => {
|
||||
host = element(by.css('hero-list-twoway'));
|
||||
});
|
||||
|
||||
it('animates between active and inactive', () => {
|
||||
addInactiveHero();
|
||||
|
||||
let li = host.element(by.css('li'));
|
||||
|
||||
expect(getScaleX(li)).toBe(1.0);
|
||||
expect(li.getCssValue('backgroundColor')).toBe(INACTIVE_COLOR);
|
||||
|
||||
li.click();
|
||||
browser.driver.sleep(300);
|
||||
expect(getScaleX(li)).toBe(1.1);
|
||||
expect(li.getCssValue('backgroundColor')).toBe(ACTIVE_COLOR);
|
||||
|
||||
li.click();
|
||||
browser.driver.sleep(300);
|
||||
expect(getScaleX(li)).toBe(1.0);
|
||||
expect(li.getCssValue('backgroundColor')).toBe(INACTIVE_COLOR);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('enter & leave', () => {
|
||||
|
||||
let host: ElementFinder;
|
||||
|
||||
beforeEach(() => {
|
||||
host = element(by.css('hero-list-enter-leave'));
|
||||
});
|
||||
|
||||
it('adds and removes element', () => {
|
||||
addInactiveHero();
|
||||
|
||||
let li = host.element(by.css('li'));
|
||||
expect(li.getCssValue('transform')).toMatch(NO_TRANSFORM_MATRIX_REGEX);
|
||||
|
||||
removeHero();
|
||||
expect(li.isPresent()).toBe(false);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('enter & leave & states', () => {
|
||||
|
||||
let host: ElementFinder;
|
||||
|
||||
beforeEach(function() {
|
||||
host = element(by.css('hero-list-enter-leave-states'));
|
||||
});
|
||||
|
||||
it('adds and removes and animates between active and inactive', () => {
|
||||
addInactiveHero();
|
||||
|
||||
let li = host.element(by.css('li'));
|
||||
|
||||
expect(li.getCssValue('transform')).toMatch(NO_TRANSFORM_MATRIX_REGEX);
|
||||
|
||||
li.click();
|
||||
browser.driver.sleep(300);
|
||||
expect(getScaleX(li)).toBe(1.1);
|
||||
|
||||
li.click();
|
||||
browser.driver.sleep(300);
|
||||
expect(li.getCssValue('transform')).toMatch(NO_TRANSFORM_MATRIX_REGEX);
|
||||
|
||||
removeHero();
|
||||
expect(li.isPresent()).toBe(false);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('auto style calc', () => {
|
||||
|
||||
let host: ElementFinder;
|
||||
|
||||
beforeEach(function() {
|
||||
host = element(by.css('hero-list-auto'));
|
||||
});
|
||||
|
||||
it('adds and removes element', () => {
|
||||
addInactiveHero();
|
||||
|
||||
let li = host.element(by.css('li'));
|
||||
expect(li.getCssValue('height')).toBe('50px');
|
||||
|
||||
removeHero();
|
||||
expect(li.isPresent()).toBe(false);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('different timings', () => {
|
||||
|
||||
let host: ElementFinder;
|
||||
|
||||
beforeEach(() => {
|
||||
host = element(by.css('hero-list-timings'));
|
||||
});
|
||||
|
||||
it('adds and removes element', () => {
|
||||
addInactiveHero();
|
||||
|
||||
let li = host.element(by.css('li'));
|
||||
expect(li.getCssValue('transform')).toMatch(NO_TRANSFORM_MATRIX_REGEX);
|
||||
expect(li.getCssValue('opacity')).toMatch('1');
|
||||
|
||||
removeHero();
|
||||
expect(li.isPresent()).toBe(false);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('multiple keyframes', () => {
|
||||
|
||||
let host: ElementFinder;
|
||||
|
||||
beforeEach(() => {
|
||||
host = element(by.css('hero-list-multistep'));
|
||||
});
|
||||
|
||||
it('adds and removes element', () => {
|
||||
addInactiveHero();
|
||||
|
||||
let li = host.element(by.css('li'));
|
||||
expect(li.getCssValue('transform')).toMatch(NO_TRANSFORM_MATRIX_REGEX);
|
||||
expect(li.getCssValue('opacity')).toMatch('1');
|
||||
|
||||
removeHero();
|
||||
expect(li.isPresent()).toBe(false);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('parallel groups', () => {
|
||||
|
||||
let host: ElementFinder;
|
||||
|
||||
beforeEach(() => {
|
||||
host = element(by.css('hero-list-groups'));
|
||||
});
|
||||
|
||||
it('adds and removes element', () => {
|
||||
addInactiveHero();
|
||||
|
||||
let li = host.element(by.css('li'));
|
||||
expect(li.getCssValue('transform')).toMatch(NO_TRANSFORM_MATRIX_REGEX);
|
||||
expect(li.getCssValue('opacity')).toMatch('1');
|
||||
|
||||
removeHero(700);
|
||||
expect(li.isPresent()).toBe(false);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('adding active heroes', () => {
|
||||
|
||||
let host: ElementFinder;
|
||||
|
||||
beforeEach(() => {
|
||||
host = element(by.css('hero-list-basic'));
|
||||
});
|
||||
|
||||
it('animates between active and inactive', () => {
|
||||
addActiveHero();
|
||||
|
||||
let li = host.element(by.css('li'));
|
||||
|
||||
expect(getScaleX(li)).toBe(1.1);
|
||||
expect(li.getCssValue('backgroundColor')).toBe(ACTIVE_COLOR);
|
||||
|
||||
li.click();
|
||||
browser.driver.sleep(300);
|
||||
expect(getScaleX(li)).toBe(1.0);
|
||||
expect(li.getCssValue('backgroundColor')).toBe(INACTIVE_COLOR);
|
||||
|
||||
li.click();
|
||||
browser.driver.sleep(300);
|
||||
expect(getScaleX(li)).toBe(1.1);
|
||||
expect(li.getCssValue('backgroundColor')).toBe(ACTIVE_COLOR);
|
||||
});
|
||||
});
|
||||
|
||||
describe('callbacks', () => {
|
||||
it('fires a callback on start and done', () => {
|
||||
addActiveHero();
|
||||
browser.manage().logs().get(logging.Type.BROWSER)
|
||||
.then((logs: logging.Entry[]) => {
|
||||
const animationMessages = logs.filter((log) => {
|
||||
return log.message.indexOf('Animation') !== -1 ? true : false;
|
||||
});
|
||||
|
||||
expect(animationMessages.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function addActiveHero(sleep?: number) {
|
||||
sleep = sleep || 500;
|
||||
element(by.buttonText('Add active hero')).click();
|
||||
browser.driver.sleep(sleep);
|
||||
}
|
||||
|
||||
function addInactiveHero(sleep?: number) {
|
||||
sleep = sleep || 500;
|
||||
element(by.buttonText('Add inactive hero')).click();
|
||||
browser.driver.sleep(sleep);
|
||||
}
|
||||
|
||||
function removeHero(sleep?: number) {
|
||||
sleep = sleep || 500;
|
||||
element(by.buttonText('Remove hero')).click();
|
||||
browser.driver.sleep(sleep);
|
||||
}
|
||||
|
||||
function getScaleX(el: ElementFinder) {
|
||||
return Promise.all([
|
||||
getBoundingClientWidth(el),
|
||||
getOffsetWidth(el)
|
||||
]).then(function(promiseResolutions) {
|
||||
let clientWidth = promiseResolutions[0];
|
||||
let offsetWidth = promiseResolutions[1];
|
||||
return clientWidth / offsetWidth;
|
||||
});
|
||||
}
|
||||
|
||||
function getBoundingClientWidth(el: ElementFinder): promise.Promise<number> {
|
||||
return browser.executeScript(
|
||||
'return arguments[0].getBoundingClientRect().width',
|
||||
el.getWebElement()
|
||||
);
|
||||
}
|
||||
|
||||
function getOffsetWidth(el: ElementFinder): promise.Promise<number> {
|
||||
return browser.executeScript(
|
||||
'return arguments[0].offsetWidth',
|
||||
el.getWebElement()
|
||||
);
|
||||
}
|
||||
});
|
@ -1,351 +0,0 @@
|
||||
'use strict'; // necessary for es6 output in node
|
||||
|
||||
import { browser, element, by, ElementFinder } from 'protractor';
|
||||
import { logging, promise } from 'selenium-webdriver';
|
||||
|
||||
/**
|
||||
* The tests here basically just checking that the end styles
|
||||
* of each animation are in effect.
|
||||
*
|
||||
* Relies on the Angular testability only becoming stable once
|
||||
* animation(s) have finished.
|
||||
*
|
||||
* Ideally we'd use https://developer.mozilla.org/en-US/docs/Web/API/Document/getAnimations
|
||||
* but they're not supported in Chrome at the moment. The upcoming nganimate polyfill
|
||||
* may also add some introspection support.
|
||||
*/
|
||||
describe('Animation Tests', () => {
|
||||
|
||||
const INACTIVE_COLOR = 'rgba(238, 238, 238, 1)';
|
||||
const ACTIVE_COLOR = 'rgba(207, 216, 220, 1)';
|
||||
const NO_TRANSFORM_MATRIX_REGEX = /matrix\(1,\s*0,\s*0,\s*1,\s*0,\s*0\)/;
|
||||
|
||||
beforeEach(() => {
|
||||
browser.get('');
|
||||
});
|
||||
|
||||
describe('basic states', () => {
|
||||
|
||||
let host: ElementFinder;
|
||||
|
||||
beforeEach(() => {
|
||||
host = element(by.css('app-hero-list-basic'));
|
||||
});
|
||||
|
||||
it('animates between active and inactive', () => {
|
||||
addInactiveHero();
|
||||
|
||||
let li = host.element(by.css('li'));
|
||||
|
||||
expect(getScaleX(li)).toBe(1.0);
|
||||
expect(li.getCssValue('backgroundColor')).toBe(INACTIVE_COLOR);
|
||||
|
||||
li.click();
|
||||
browser.driver.sleep(300);
|
||||
expect(getScaleX(li)).toBe(1.1);
|
||||
expect(li.getCssValue('backgroundColor')).toBe(ACTIVE_COLOR);
|
||||
|
||||
li.click();
|
||||
browser.driver.sleep(300);
|
||||
expect(getScaleX(li)).toBe(1.0);
|
||||
expect(li.getCssValue('backgroundColor')).toBe(INACTIVE_COLOR);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('styles inline in transitions', () => {
|
||||
|
||||
let host: ElementFinder;
|
||||
|
||||
beforeEach(function() {
|
||||
host = element(by.css('app-hero-list-inline-styles'));
|
||||
});
|
||||
|
||||
it('are not kept after animation', () => {
|
||||
addInactiveHero();
|
||||
|
||||
let li = host.element(by.css('li'));
|
||||
|
||||
li.click();
|
||||
browser.driver.sleep(300);
|
||||
expect(getScaleX(li)).toBe(1.0);
|
||||
expect(li.getCssValue('backgroundColor')).toBe(INACTIVE_COLOR);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('combined transition syntax', () => {
|
||||
|
||||
let host: ElementFinder;
|
||||
|
||||
beforeEach(() => {
|
||||
host = element(by.css('app-hero-list-combined-transitions'));
|
||||
});
|
||||
|
||||
it('animates between active and inactive', () => {
|
||||
addInactiveHero();
|
||||
|
||||
let li = host.element(by.css('li'));
|
||||
|
||||
expect(getScaleX(li)).toBe(1.0);
|
||||
expect(li.getCssValue('backgroundColor')).toBe(INACTIVE_COLOR);
|
||||
|
||||
li.click();
|
||||
browser.driver.sleep(300);
|
||||
expect(getScaleX(li)).toBe(1.1);
|
||||
expect(li.getCssValue('backgroundColor')).toBe(ACTIVE_COLOR);
|
||||
|
||||
li.click();
|
||||
browser.driver.sleep(300);
|
||||
expect(getScaleX(li)).toBe(1.0);
|
||||
expect(li.getCssValue('backgroundColor')).toBe(INACTIVE_COLOR);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('two-way transition syntax', () => {
|
||||
|
||||
let host: ElementFinder;
|
||||
|
||||
beforeEach(() => {
|
||||
host = element(by.css('app-hero-list-twoway'));
|
||||
});
|
||||
|
||||
it('animates between active and inactive', () => {
|
||||
addInactiveHero();
|
||||
|
||||
let li = host.element(by.css('li'));
|
||||
|
||||
expect(getScaleX(li)).toBe(1.0);
|
||||
expect(li.getCssValue('backgroundColor')).toBe(INACTIVE_COLOR);
|
||||
|
||||
li.click();
|
||||
browser.driver.sleep(300);
|
||||
expect(getScaleX(li)).toBe(1.1);
|
||||
expect(li.getCssValue('backgroundColor')).toBe(ACTIVE_COLOR);
|
||||
|
||||
li.click();
|
||||
browser.driver.sleep(300);
|
||||
expect(getScaleX(li)).toBe(1.0);
|
||||
expect(li.getCssValue('backgroundColor')).toBe(INACTIVE_COLOR);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('enter & leave', () => {
|
||||
|
||||
let host: ElementFinder;
|
||||
|
||||
beforeEach(() => {
|
||||
host = element(by.css('app-hero-list-enter-leave'));
|
||||
});
|
||||
|
||||
it('adds and removes element', () => {
|
||||
addInactiveHero();
|
||||
|
||||
let li = host.element(by.css('li'));
|
||||
expect(li.getCssValue('transform')).toMatch(NO_TRANSFORM_MATRIX_REGEX);
|
||||
|
||||
removeHero();
|
||||
expect(li.isPresent()).toBe(false);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('enter & leave & states', () => {
|
||||
|
||||
let host: ElementFinder;
|
||||
|
||||
beforeEach(function() {
|
||||
host = element(by.css('app-hero-list-enter-leave-states'));
|
||||
});
|
||||
|
||||
it('adds and removes and animates between active and inactive', () => {
|
||||
addInactiveHero();
|
||||
|
||||
let li = host.element(by.css('li'));
|
||||
|
||||
expect(li.getCssValue('transform')).toMatch(NO_TRANSFORM_MATRIX_REGEX);
|
||||
|
||||
li.click();
|
||||
browser.driver.sleep(300);
|
||||
expect(getScaleX(li)).toBe(1.1);
|
||||
|
||||
li.click();
|
||||
browser.driver.sleep(300);
|
||||
expect(li.getCssValue('transform')).toMatch(NO_TRANSFORM_MATRIX_REGEX);
|
||||
|
||||
removeHero();
|
||||
expect(li.isPresent()).toBe(false);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('auto style calc', () => {
|
||||
|
||||
let host: ElementFinder;
|
||||
|
||||
beforeEach(function() {
|
||||
host = element(by.css('app-hero-list-auto'));
|
||||
});
|
||||
|
||||
it('adds and removes element', () => {
|
||||
addInactiveHero();
|
||||
|
||||
let li = host.element(by.css('li'));
|
||||
expect(li.getCssValue('height')).toBe('50px');
|
||||
|
||||
removeHero();
|
||||
expect(li.isPresent()).toBe(false);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('different timings', () => {
|
||||
|
||||
let host: ElementFinder;
|
||||
|
||||
beforeEach(() => {
|
||||
host = element(by.css('app-hero-list-timings'));
|
||||
});
|
||||
|
||||
it('adds and removes element', () => {
|
||||
addInactiveHero();
|
||||
|
||||
let li = host.element(by.css('li'));
|
||||
expect(li.getCssValue('transform')).toMatch(NO_TRANSFORM_MATRIX_REGEX);
|
||||
expect(li.getCssValue('opacity')).toMatch('1');
|
||||
|
||||
removeHero();
|
||||
expect(li.isPresent()).toBe(false);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('multiple keyframes', () => {
|
||||
|
||||
let host: ElementFinder;
|
||||
|
||||
beforeEach(() => {
|
||||
host = element(by.css('app-hero-list-multistep'));
|
||||
});
|
||||
|
||||
it('adds and removes element', () => {
|
||||
addInactiveHero();
|
||||
|
||||
let li = host.element(by.css('li'));
|
||||
expect(li.getCssValue('transform')).toMatch(NO_TRANSFORM_MATRIX_REGEX);
|
||||
expect(li.getCssValue('opacity')).toMatch('1');
|
||||
|
||||
removeHero();
|
||||
expect(li.isPresent()).toBe(false);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('parallel groups', () => {
|
||||
|
||||
let host: ElementFinder;
|
||||
|
||||
beforeEach(() => {
|
||||
host = element(by.css('app-hero-list-groups'));
|
||||
});
|
||||
|
||||
it('adds and removes element', () => {
|
||||
addInactiveHero();
|
||||
|
||||
let li = host.element(by.css('li'));
|
||||
expect(li.getCssValue('transform')).toMatch(NO_TRANSFORM_MATRIX_REGEX);
|
||||
expect(li.getCssValue('opacity')).toMatch('1');
|
||||
|
||||
removeHero(700);
|
||||
expect(li.isPresent()).toBe(false);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('adding active heroes', () => {
|
||||
|
||||
let host: ElementFinder;
|
||||
|
||||
beforeEach(() => {
|
||||
host = element(by.css('app-hero-list-basic'));
|
||||
});
|
||||
|
||||
it('animates between active and inactive', () => {
|
||||
addActiveHero();
|
||||
|
||||
let li = host.element(by.css('li'));
|
||||
|
||||
expect(getScaleX(li)).toBe(1.1);
|
||||
expect(li.getCssValue('backgroundColor')).toBe(ACTIVE_COLOR);
|
||||
|
||||
li.click();
|
||||
browser.driver.sleep(300);
|
||||
expect(getScaleX(li)).toBe(1.0);
|
||||
expect(li.getCssValue('backgroundColor')).toBe(INACTIVE_COLOR);
|
||||
|
||||
li.click();
|
||||
browser.driver.sleep(300);
|
||||
expect(getScaleX(li)).toBe(1.1);
|
||||
expect(li.getCssValue('backgroundColor')).toBe(ACTIVE_COLOR);
|
||||
});
|
||||
});
|
||||
|
||||
describe('callbacks', () => {
|
||||
it('fires a callback on start and done', () => {
|
||||
addActiveHero();
|
||||
browser.manage().logs().get(logging.Type.BROWSER)
|
||||
.then((logs: logging.Entry[]) => {
|
||||
const animationMessages = logs.filter((log) => {
|
||||
return log.message.indexOf('Animation') !== -1 ? true : false;
|
||||
});
|
||||
|
||||
expect(animationMessages.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function addActiveHero(sleep?: number) {
|
||||
sleep = sleep || 500;
|
||||
element(by.buttonText('Add active hero')).click();
|
||||
browser.driver.sleep(sleep);
|
||||
}
|
||||
|
||||
function addInactiveHero(sleep?: number) {
|
||||
sleep = sleep || 500;
|
||||
element(by.buttonText('Add inactive hero')).click();
|
||||
browser.driver.sleep(sleep);
|
||||
}
|
||||
|
||||
function removeHero(sleep?: number) {
|
||||
sleep = sleep || 500;
|
||||
element(by.buttonText('Remove hero')).click();
|
||||
browser.driver.sleep(sleep);
|
||||
}
|
||||
|
||||
function getScaleX(el: ElementFinder) {
|
||||
return Promise.all([
|
||||
getBoundingClientWidth(el),
|
||||
getOffsetWidth(el)
|
||||
]).then(function(promiseResolutions) {
|
||||
let clientWidth = promiseResolutions[0];
|
||||
let offsetWidth = promiseResolutions[1];
|
||||
return clientWidth / offsetWidth;
|
||||
});
|
||||
}
|
||||
|
||||
function getBoundingClientWidth(el: ElementFinder) {
|
||||
return browser.executeScript(
|
||||
'return arguments[0].getBoundingClientRect().width',
|
||||
el.getWebElement()
|
||||
) as PromiseLike<number>;
|
||||
}
|
||||
|
||||
function getOffsetWidth(el: ElementFinder) {
|
||||
return browser.executeScript(
|
||||
'return arguments[0].offsetWidth',
|
||||
el.getWebElement()
|
||||
) as PromiseLike<number>;
|
||||
}
|
||||
});
|
8
aio/content/examples/animations/plnkr.json
Normal file
8
aio/content/examples/animations/plnkr.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"description": "Angular Animations",
|
||||
"basePath": "src/",
|
||||
"files":[
|
||||
"!**/*.d.ts",
|
||||
"!**/*.js"
|
||||
]
|
||||
}
|
@ -1,6 +1,5 @@
|
||||
// #docplaster
|
||||
import { NgModule } from '@angular/core';
|
||||
// #docregion animations-module
|
||||
import { NgModule } from '@angular/core';
|
||||
import { BrowserModule } from '@angular/platform-browser';
|
||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||
// #enddocregion animations-module
|
||||
@ -16,12 +15,11 @@ import { HeroListAutoComponent } from './hero-list-auto.component';
|
||||
import { HeroListGroupsComponent } from './hero-list-groups.component';
|
||||
import { HeroListMultistepComponent } from './hero-list-multistep.component';
|
||||
import { HeroListTimingsComponent } from './hero-list-timings.component';
|
||||
// #docregion animations-module
|
||||
|
||||
// #docregion animation-module
|
||||
@NgModule({
|
||||
imports: [ BrowserModule, BrowserAnimationsModule ],
|
||||
// ... more stuff ...
|
||||
// #enddocregion animations-module
|
||||
// #enddocregion animation-module
|
||||
declarations: [
|
||||
HeroTeamBuilderComponent,
|
||||
HeroListBasicComponent,
|
||||
@ -36,8 +34,5 @@ import { HeroListTimingsComponent } from './hero-list-timings.component';
|
||||
HeroListGroupsComponent
|
||||
],
|
||||
bootstrap: [ HeroTeamBuilderComponent ]
|
||||
// #docregion animations-module
|
||||
})
|
||||
export class AppModule { }
|
||||
// #enddocregion animations-module
|
||||
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user