diff --git a/aio/aio-builds-setup/build.sh b/aio/aio-builds-setup/build.sh new file mode 100755 index 0000000000..9c466308ce --- /dev/null +++ b/aio/aio-builds-setup/build.sh @@ -0,0 +1,18 @@ +#!/bin/bash + +set -eux -o pipefail + +# Constants +DOCKERBUILD_DIR="`dirname $0`/dockerbuild" +SCRIPTS_JS_DIR="$DOCKERBUILD_DIR/scripts-js" +DEFAULT_IMAGE_NAME_AND_TAG="aio-builds:latest" + +# Build `scripts-js/` +cd "$SCRIPTS_JS_DIR" +yarn install +yarn run build +cd - + +# Create docker image +nameAndOptionalTag=$([ $# -eq 0 ] && echo $DEFAULT_IMAGE_NAME_AND_TAG || echo $1) +sudo docker build --tag $nameAndOptionalTag $DOCKERBUILD_DIR diff --git a/aio/aio-builds-setup/dockerbuild/.dockerignore b/aio/aio-builds-setup/dockerbuild/.dockerignore new file mode 100644 index 0000000000..a53938b615 --- /dev/null +++ b/aio/aio-builds-setup/dockerbuild/.dockerignore @@ -0,0 +1,3 @@ +scripts-js/lib +scripts-js/node_modules +scripts-js/**/test diff --git a/aio/aio-builds-setup/dockerbuild/Dockerfile b/aio/aio-builds-setup/dockerbuild/Dockerfile new file mode 100644 index 0000000000..a69b76d248 --- /dev/null +++ b/aio/aio-builds-setup/dockerbuild/Dockerfile @@ -0,0 +1,112 @@ +# Image metadata and config +FROM debian:jessie + +LABEL name="angular.io PR preview" \ + description="This image implements the PR preview functionality for angular.io." \ + vendor="Angular" \ + version="1.0" + +VOLUME /var/www/aio-builds + +EXPOSE 80 443 + +ENV AIO_BUILDS_DIR=/var/www/aio-builds TEST_AIO_BUILDS_DIR=/tmp/aio-builds \ + AIO_GITHUB_TOKEN= TEST_AIO_GITHUB_TOKEN= \ + AIO_NGINX_HOSTNAME=nginx-prod.localhost TEST_AIO_NGINX_HOSTNAME=nginx-test.localhost \ + AIO_NGINX_PORT_HTTP=80 TEST_AIO_NGINX_PORT_HTTP=8080 \ + AIO_NGINX_PORT_HTTPS=443 TEST_AIO_NGINX_PORT_HTTPS=4433 \ + AIO_REPO_SLUG=angular/angular TEST_AIO_REPO_SLUG= \ + AIO_SCRIPTS_JS_DIR=/usr/share/aio-scripts-js \ + AIO_SCRIPTS_SH_DIR=/usr/share/aio-scripts-sh \ + AIO_UPLOAD_HOSTNAME=upload-prod.localhost TEST_AIO_UPLOAD_HOSTNAME=upload-test.localhost \ + AIO_UPLOAD_MAX_SIZE=20971520 TEST_AIO_UPLOAD_MAX_SIZE=20971520 \ + AIO_UPLOAD_PORT=3000 TEST_AIO_UPLOAD_PORT=3001 \ + NODE_ENV=production + + +# Create directory for logs +RUN mkdir /var/log/aio + + +# Add extra package sources +RUN apt-get update -y && apt-get install -y curl +RUN curl --silent --show-error --location https://deb.nodesource.com/setup_6.x | bash - +RUN curl --silent --show-error https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - +RUN echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list + + +# Install packages +RUN apt-get update -y && apt-get install -y \ + chkconfig \ + cron \ + dnsmasq \ + nano \ + nginx \ + nodejs \ + rsyslog \ + yarn +RUN yarn global add pm2 + + +# Set up cronjobs +COPY cronjobs/aio-builds-cleanup /etc/cron.d/ +RUN chmod 0744 /etc/cron.d/aio-builds-cleanup +RUN crontab /etc/cron.d/aio-builds-cleanup + + +# Set up dnsmasq +COPY dnsmasq/dnsmasq.conf /etc/dnsmasq.conf +RUN sed -i "s|{{\$AIO_NGINX_HOSTNAME}}|$AIO_NGINX_HOSTNAME|" /etc/dnsmasq.conf +RUN sed -i "s|{{\$AIO_UPLOAD_HOSTNAME}}|$AIO_UPLOAD_HOSTNAME|" /etc/dnsmasq.conf +RUN sed -i "s|{{\$TEST_AIO_NGINX_HOSTNAME}}|$TEST_AIO_NGINX_HOSTNAME|" /etc/dnsmasq.conf +RUN sed -i "s|{{\$TEST_AIO_UPLOAD_HOSTNAME}}|$TEST_AIO_UPLOAD_HOSTNAME|" /etc/dnsmasq.conf + + +# Set up nginx (for production and testing) +RUN rm /etc/nginx/sites-enabled/* + +COPY nginx/aio-builds.conf /etc/nginx/sites-available/aio-builds-prod.conf +RUN sed -i "s|{{\$AIO_BUILDS_DIR}}|$AIO_BUILDS_DIR|" /etc/nginx/sites-available/aio-builds-prod.conf +RUN sed -i "s|{{\$AIO_NGINX_PORT_HTTP}}|$AIO_NGINX_PORT_HTTP|" /etc/nginx/sites-available/aio-builds-prod.conf +RUN sed -i "s|{{\$AIO_UPLOAD_HOSTNAME}}|$AIO_UPLOAD_HOSTNAME|" /etc/nginx/sites-available/aio-builds-prod.conf +RUN sed -i "s|{{\$AIO_UPLOAD_MAX_SIZE}}|$AIO_UPLOAD_MAX_SIZE|" /etc/nginx/sites-available/aio-builds-prod.conf +RUN sed -i "s|{{\$AIO_UPLOAD_PORT}}|$AIO_UPLOAD_PORT|" /etc/nginx/sites-available/aio-builds-prod.conf +RUN ln -s /etc/nginx/sites-available/aio-builds-prod.conf /etc/nginx/sites-enabled/aio-builds-prod.conf + +COPY nginx/aio-builds.conf /etc/nginx/sites-available/aio-builds-test.conf +RUN sed -i "s|{{\$AIO_BUILDS_DIR}}|$TEST_AIO_BUILDS_DIR|" /etc/nginx/sites-available/aio-builds-test.conf +RUN sed -i "s|{{\$AIO_NGINX_PORT_HTTP}}|$TEST_AIO_NGINX_PORT_HTTP|" /etc/nginx/sites-available/aio-builds-test.conf +RUN sed -i "s|{{\$AIO_UPLOAD_HOSTNAME}}|$TEST_AIO_UPLOAD_HOSTNAME|" /etc/nginx/sites-available/aio-builds-test.conf +RUN sed -i "s|{{\$AIO_UPLOAD_MAX_SIZE}}|$TEST_AIO_UPLOAD_MAX_SIZE|" /etc/nginx/sites-available/aio-builds-test.conf +RUN sed -i "s|{{\$AIO_UPLOAD_PORT}}|$TEST_AIO_UPLOAD_PORT|" /etc/nginx/sites-available/aio-builds-test.conf +RUN ln -s /etc/nginx/sites-available/aio-builds-test.conf /etc/nginx/sites-enabled/aio-builds-test.conf + + +# Set up pm2 +RUN pm2 startup systemv -u root > /dev/null \ + # Ugly! + || echo " ---> Working around https://github.com/Unitech/pm2/commit/a788e523e#commitcomment-20851443" \ + && chkconfig --add pm2 > /dev/null +RUN chkconfig pm2 on + + +# Set up the shell scripts +COPY scripts-sh/ $AIO_SCRIPTS_SH_DIR/ +RUN chmod a+x $AIO_SCRIPTS_SH_DIR/* +RUN find $AIO_SCRIPTS_SH_DIR -maxdepth 1 -type f -printf "%P\n" \ + | while read file; do ln -s $AIO_SCRIPTS_SH_DIR/$file /usr/local/bin/aio-${file%.*}; done + + +# Set up the Node.js scripts +COPY scripts-js/ $AIO_SCRIPTS_JS_DIR/ +WORKDIR $AIO_SCRIPTS_JS_DIR/ +RUN yarn install --production + + +# Set up health check +HEALTHCHECK --interval=5m CMD /usr/local/bin/aio-health-check + + +# Go! +WORKDIR / +CMD aio-init && tail -f /dev/null diff --git a/aio/aio-builds-setup/dockerbuild/cronjobs/aio-builds-cleanup b/aio/aio-builds-setup/dockerbuild/cronjobs/aio-builds-cleanup new file mode 100644 index 0000000000..1b5d9c8000 --- /dev/null +++ b/aio/aio-builds-setup/dockerbuild/cronjobs/aio-builds-cleanup @@ -0,0 +1,2 @@ +# Periodically clean up builds that do not correspond to currently open PRs +0 4 * * * root /usr/local/bin/aio-clean-up >> /var/log/cron.log 2>&1 diff --git a/aio/aio-builds-setup/dockerbuild/dnsmasq/dnsmasq.conf b/aio/aio-builds-setup/dockerbuild/dnsmasq/dnsmasq.conf new file mode 100644 index 0000000000..a675170142 --- /dev/null +++ b/aio/aio-builds-setup/dockerbuild/dnsmasq/dnsmasq.conf @@ -0,0 +1,16 @@ +# Do not read /etc/resolv.conf. Get servers from this file instead. +no-resolv +server=8.8.8.8 +server=8.8.4.4 + +# Listen for DHCP and DNS requests only on this address. +listen-address=127.0.0.1 + +# Force an IP addres for these domains. +address=/{{$AIO_NGINX_HOSTNAME}}/127.0.0.1 +address=/{{$AIO_UPLOAD_HOSTNAME}}/127.0.0.1 +address=/{{$TEST_AIO_NGINX_HOSTNAME}}/127.0.0.1 +address=/{{$TEST_AIO_UPLOAD_HOSTNAME}}/127.0.0.1 + +# Run as root (required from inside docker container). +user=root diff --git a/aio/aio-builds-setup/dockerbuild/nginx/aio-builds.conf b/aio/aio-builds-setup/dockerbuild/nginx/aio-builds.conf new file mode 100644 index 0000000000..26975a4ad9 --- /dev/null +++ b/aio/aio-builds-setup/dockerbuild/nginx/aio-builds.conf @@ -0,0 +1,56 @@ +# Serve PR-preview requests +server { + listen {{$AIO_NGINX_PORT_HTTP}}; + listen [::]:{{$AIO_NGINX_PORT_HTTP}}; + + server_name "~^pr(?[1-9][0-9]*)-(?[0-9a-f]{40})\."; + + root {{$AIO_BUILDS_DIR}}/$pr/$sha; + disable_symlinks on from=$document_root; + index index.html; + + location / { + try_files $uri $uri/ =404; + } +} + +# Handle all other requests +server { + listen {{$AIO_NGINX_PORT_HTTP}} default_server; + listen [::]:{{$AIO_NGINX_PORT_HTTP}}; + + server_name _; + + # Health check + location "~^\/health-check\/?$" { + add_header Content-Type text/plain; + return 200 ''; + } + + # Upload builds + location "~^\/create-build\/(?[1-9][0-9]*)\/(?[0-9a-f]{40})\/?$" { + if ($request_method != "POST") { + add_header Allow "POST"; + return 405; + } + + client_body_temp_path /tmp/aio-create-builds; + client_body_buffer_size 128K; + client_max_body_size {{$AIO_UPLOAD_MAX_SIZE}}; + client_body_in_file_only on; + + proxy_pass_request_headers on; + proxy_set_header X-FILE $request_body_file; + proxy_set_body off; + proxy_redirect off; + proxy_method GET; + proxy_pass http://{{$AIO_UPLOAD_HOSTNAME}}:{{$AIO_UPLOAD_PORT}}$request_uri; + + resolver 127.0.0.1; + } + + # Everything else + location / { + return 404; + } +} diff --git a/aio/aio-builds-setup/dockerbuild/scripts-js/.gitignore b/aio/aio-builds-setup/dockerbuild/scripts-js/.gitignore new file mode 100644 index 0000000000..178135c2b2 --- /dev/null +++ b/aio/aio-builds-setup/dockerbuild/scripts-js/.gitignore @@ -0,0 +1 @@ +/dist/ diff --git a/aio/aio-builds-setup/dockerbuild/scripts-js/lib/clean-up/build-cleaner.ts b/aio/aio-builds-setup/dockerbuild/scripts-js/lib/clean-up/build-cleaner.ts new file mode 100644 index 0000000000..df17f8133d --- /dev/null +++ b/aio/aio-builds-setup/dockerbuild/scripts-js/lib/clean-up/build-cleaner.ts @@ -0,0 +1,72 @@ +// Imports +import * as fs from 'fs'; +import * as path from 'path'; +import * as shell from 'shelljs'; +import {GithubPullRequests} from '../common/github-pull-requests'; + +// Classes +export class BuildCleaner { + // Constructor + constructor(protected buildsDir: string, protected repoSlug: string, protected githubToken?: string) { + if (!buildsDir) { + throw new Error('Missing required parameter \'buildsDir\'!'); + } else if (!repoSlug) { + throw new Error('Missing required parameter \'repoSlug\'!'); + } + } + + // Methods - Public + public cleanUp(): Promise { + return Promise.all([ + this.getExistingBuildNumbers(), + this.getOpenPrNumbers(), + ]).then(([existingBuilds, openPrs]) => this.removeUnnecessaryBuilds(existingBuilds, openPrs)); + } + + // Methods - Protected + protected getExistingBuildNumbers(): Promise { + return new Promise((resolve, reject) => { + fs.readdir(this.buildsDir, (err, files) => { + if (err) { + return reject(err); + } + + const buildNumbers = files. + map(Number). // Convert string to number + filter(Boolean); // Ignore NaN (or 0), because they are not builds + + resolve(buildNumbers); + }); + }); + } + + protected getOpenPrNumbers(): Promise { + const githubPullRequests = new GithubPullRequests(this.repoSlug, this.githubToken); + + return githubPullRequests. + fetchAll('open'). + then(prs => prs.map(pr => pr.number)); + } + + protected removeDir(dir: string) { + try { + // Undocumented signature (see https://github.com/shelljs/shelljs/pull/663). + (shell as any).chmod('-R', 'a+w', dir); + shell.rm('-rf', dir); + } catch (err) { + console.error(`ERROR: Unable to remove '${dir}' due to:`, err); + } + } + + protected removeUnnecessaryBuilds(existingBuildNumbers: number[], openPrNumbers: number[]) { + const toRemove = existingBuildNumbers.filter(num => !openPrNumbers.includes(num)); + + console.log(`Existing builds: ${existingBuildNumbers.length}`); + console.log(`Open pull requests: ${openPrNumbers.length}`); + console.log(`Removing ${toRemove.length} build(s): ${toRemove.join(', ')}`); + + toRemove. + map(num => path.join(this.buildsDir, String(num))). + forEach(dir => this.removeDir(dir)); + } +} diff --git a/aio/aio-builds-setup/dockerbuild/scripts-js/lib/clean-up/index.ts b/aio/aio-builds-setup/dockerbuild/scripts-js/lib/clean-up/index.ts new file mode 100644 index 0000000000..ed43490ec3 --- /dev/null +++ b/aio/aio-builds-setup/dockerbuild/scripts-js/lib/clean-up/index.ts @@ -0,0 +1,21 @@ +// Imports +import {getEnvVar} from '../common/utils'; +import {BuildCleaner} from './build-cleaner'; + +// Constants +const AIO_BUILDS_DIR = getEnvVar('AIO_BUILDS_DIR'); +const AIO_GITHUB_TOKEN = getEnvVar('AIO_GITHUB_TOKEN', true); +const AIO_REPO_SLUG = getEnvVar('AIO_REPO_SLUG'); + +// Run +_main(); + +// Functions +function _main() { + const buildCleaner = new BuildCleaner(AIO_BUILDS_DIR, AIO_REPO_SLUG, AIO_GITHUB_TOKEN); + + buildCleaner.cleanUp().catch(err => { + console.error('ERROR:', err); + process.exit(1); + }); +} diff --git a/aio/aio-builds-setup/dockerbuild/scripts-js/lib/common/github-api.ts b/aio/aio-builds-setup/dockerbuild/scripts-js/lib/common/github-api.ts new file mode 100644 index 0000000000..d103e9d87a --- /dev/null +++ b/aio/aio-builds-setup/dockerbuild/scripts-js/lib/common/github-api.ts @@ -0,0 +1,97 @@ +// Imports +import {IncomingMessage} from 'http'; +import * as https from 'https'; + +// Constants +const GITHUB_HOSTNAME = 'api.github.com'; + +// Interfaces - Types +interface RequestParams { + [key: string]: string | number; +} + +type RequestParamsOrNull = RequestParams | null; + +// Classes +export class GithubApi { + protected requestHeaders: {[key: string]: string}; + + // Constructor + constructor(protected repoSlug: string, githubToken?: string) { + if (!repoSlug) { + throw new Error('Missing required parameter \'repoSlug\'!'); + } + if (!githubToken) { + console.warn('No GitHub access-token specified. Requests will be unauthenticated.'); + } + + this.requestHeaders = {'User-Agent': `Node/${process.versions.node}`}; + if (githubToken) { + this.requestHeaders['Authorization'] = `token ${githubToken}`; + } + } + + // Methods - Public + public get(pathname: string, params?: RequestParamsOrNull): Promise { + const path = this.buildPath(pathname, params); + return this.request('get', path); + } + + public post(pathname: string, params?: RequestParamsOrNull, data?: any): Promise { + const path = this.buildPath(pathname, params); + return this.request('post', path, data); + } + + // Methods - Protected + protected buildPath(pathname: string, params?: RequestParamsOrNull): string { + if (params == null) { + return pathname; + } + + const search = (params === null) ? '' : this.serializeSearchParams(params); + const joiner = search && '?'; + + return `${pathname}${joiner}${search}`; + } + + protected request(method: string, path: string, data: any = null): Promise { + return new Promise((resolve, reject) => { + const options = { + headers: {...this.requestHeaders}, + host: GITHUB_HOSTNAME, + method, + path, + }; + + const onError = (statusCode: number, responseText: string) => { + const url = `https://${GITHUB_HOSTNAME}${path}`; + reject(`Request to '${url}' failed (status: ${statusCode}): ${responseText}`); + }; + const onSuccess = (responseText: string) => { + try { resolve(JSON.parse(responseText)); } catch (err) { reject(err); } + }; + const onResponse = (res: IncomingMessage) => { + const statusCode = res.statusCode || -1; + const isSuccess = (200 <= statusCode) && (statusCode < 400); + let responseText = ''; + + res. + on('data', d => responseText += d). + on('end', () => isSuccess ? onSuccess(responseText) : onError(statusCode, responseText)). + on('error', reject); + }; + + https. + request(options, onResponse). + on('error', reject). + end(data && JSON.stringify(data)); + }); + } + + protected serializeSearchParams(params: RequestParams): string { + return Object.keys(params). + filter(key => params[key] != null). + map(key => `${key}=${encodeURIComponent(String(params[key]))}`). + join('&'); + } +} diff --git a/aio/aio-builds-setup/dockerbuild/scripts-js/lib/common/github-pull-requests.ts b/aio/aio-builds-setup/dockerbuild/scripts-js/lib/common/github-pull-requests.ts new file mode 100644 index 0000000000..a93ea73da8 --- /dev/null +++ b/aio/aio-builds-setup/dockerbuild/scripts-js/lib/common/github-pull-requests.ts @@ -0,0 +1,51 @@ +// Imports +import {GithubApi} from './github-api'; + +// Interfaces - Types +interface PullRequest { + number: number; +} + +export type PullRequestState = 'all' | 'closed' | 'open'; + +// Classes +export class GithubPullRequests extends GithubApi { + // Methods - Public + public addComment(pr: number, body: string): Promise { + if (!(pr > 0)) { + throw new Error(`Invalid PR number: ${pr}`); + } else if (!body) { + throw new Error(`Invalid or empty comment body: ${body}`); + } + + return this.post(`/repos/${this.repoSlug}/issues/${pr}/comments`, null, {body}); + } + + public fetchAll(state: PullRequestState = 'all'): Promise { + process.stdout.write(`Fetching ${state} pull requests...`); + return this.fetchUntilDone(state, 0); + } + + // Methods - Protected + protected fetchUntilDone(state: PullRequestState, currentPage: number): Promise { + process.stdout.write('.'); + + const perPage = 100; + const pathname = `/repos/${this.repoSlug}/pulls`; + const params = { + page: currentPage, + per_page: perPage, + state, + }; + + return this.get(pathname, params).then(pullRequests => { + if (pullRequests.length < perPage) { + console.log('done'); + return pullRequests; + } + + return this.fetchUntilDone(state, currentPage + 1). + then(morePullRequests => [...pullRequests, ...morePullRequests]); + }); + } +} diff --git a/aio/aio-builds-setup/dockerbuild/scripts-js/lib/common/run-tests.ts b/aio/aio-builds-setup/dockerbuild/scripts-js/lib/common/run-tests.ts new file mode 100644 index 0000000000..296b910d39 --- /dev/null +++ b/aio/aio-builds-setup/dockerbuild/scripts-js/lib/common/run-tests.ts @@ -0,0 +1,23 @@ +export const runTests = (specFiles: string[], helpers?: string[]) => { + // We can't use `import` here, because of the following mess: + // - GitHub project `jasmine/jasmine` is `jasmine-core` on npm and its typings `@types/jasmine`. + // - GitHub project `jasmine/jasmine-npm` is `jasmine` on npm and has no typings. + // + // Using `import...from 'jasmine'` here, would import from `@types/jasmine` (which refers to the + // `jasmine-core` module and the `jasmine` module). + // tslint:disable-next-line: no-var-requires variable-name + const Jasmine = require('jasmine'); + const config = { + helpers, + random: true, + spec_files: specFiles, + stopSpecOnExpectationFailure: true, + }; + + process.on('unhandledRejection', (reason: any) => console.log('Unhandled rejection:', reason)); + + const runner = new Jasmine(); + runner.loadConfig(config); + runner.onComplete((passed: boolean) => process.exit(passed ? 0 : 1)); + runner.execute(); +}; diff --git a/aio/aio-builds-setup/dockerbuild/scripts-js/lib/common/utils.ts b/aio/aio-builds-setup/dockerbuild/scripts-js/lib/common/utils.ts new file mode 100644 index 0000000000..58e92ef35f --- /dev/null +++ b/aio/aio-builds-setup/dockerbuild/scripts-js/lib/common/utils.ts @@ -0,0 +1,11 @@ +// Functions +export const getEnvVar = (name: string, isOptional = false): string => { + const value = process.env[name]; + + if (!isOptional && !value) { + console.error(`ERROR: Missing required environment variable '${name}'!`); + process.exit(1); + } + + return value || ''; +}; diff --git a/aio/aio-builds-setup/dockerbuild/scripts-js/lib/upload-server/build-creator.ts b/aio/aio-builds-setup/dockerbuild/scripts-js/lib/upload-server/build-creator.ts new file mode 100644 index 0000000000..ac623ebd50 --- /dev/null +++ b/aio/aio-builds-setup/dockerbuild/scripts-js/lib/upload-server/build-creator.ts @@ -0,0 +1,83 @@ +// Imports +import * as cp from 'child_process'; +import {EventEmitter} from 'events'; +import * as fs from 'fs'; +import * as path from 'path'; +import * as shell from 'shelljs'; +import {CreatedBuildEvent} from './build-events'; +import {UploadError} from './upload-error'; + +// Classes +export class BuildCreator extends EventEmitter { + // Constructor + constructor(protected buildsDir: string) { + super(); + + if (!buildsDir) { + throw new Error('Missing or empty required parameter \'buildsDir\'!'); + } + } + + // Methods - Public + public create(pr: string, sha: string, archivePath: string): Promise { + const prDir = path.join(this.buildsDir, pr); + const shaDir = path.join(prDir, sha); + let dirToRemoveOnError: string; + + return Promise. + all([this.exists(prDir), this.exists(shaDir)]). + then(([prDirExisted, shaDirExisted]) => { + if (shaDirExisted) { + throw new UploadError(403, `Request to overwrite existing directory: ${shaDir}`); + } + + dirToRemoveOnError = prDirExisted ? shaDir : prDir; + + return Promise.resolve(). + then(() => shell.mkdir('-p', shaDir)). + then(() => this.extractArchive(archivePath, shaDir)). + then(() => this.emit(CreatedBuildEvent.type, new CreatedBuildEvent(+pr, sha))); + }). + catch(err => { + if (dirToRemoveOnError) { + shell.rm('-rf', dirToRemoveOnError); + } + + if (!(err instanceof UploadError)) { + err = new UploadError(500, `Error while uploading to directory: ${shaDir}\n${err}`); + } + + throw err; + }); + } + + // Methods - Protected + protected exists(fileOrDir: string): Promise { + return new Promise(resolve => fs.access(fileOrDir, err => resolve(!err))); + } + + protected extractArchive(inputFile: string, outputDir: string): Promise { + return new Promise((resolve, reject) => { + const cmd = `tar --extract --gzip --directory "${outputDir}" --file "${inputFile}"`; + + cp.exec(cmd, (err, _stdout, stderr) => { + if (err) { + return reject(err); + } + + if (stderr) { + console.warn(stderr); + } + + try { + // Undocumented signature (see https://github.com/shelljs/shelljs/pull/663). + (shell as any).chmod('-R', 'a-w', outputDir); + shell.rm('-f', inputFile); + resolve(); + } catch (err) { + reject(err); + } + }); + }); + } +} diff --git a/aio/aio-builds-setup/dockerbuild/scripts-js/lib/upload-server/build-events.ts b/aio/aio-builds-setup/dockerbuild/scripts-js/lib/upload-server/build-events.ts new file mode 100644 index 0000000000..b1d8b17bbf --- /dev/null +++ b/aio/aio-builds-setup/dockerbuild/scripts-js/lib/upload-server/build-events.ts @@ -0,0 +1,15 @@ +// Classes +export class BuildEvent { + // Constructor + constructor(public type: string, public pr: number, public sha: string) {} +} + +export class CreatedBuildEvent extends BuildEvent { + // Properties - Public, Static + public static type = 'build.created'; + + // Constructor + constructor(pr: number, sha: string) { + super(CreatedBuildEvent.type, pr, sha); + } +} diff --git a/aio/aio-builds-setup/dockerbuild/scripts-js/lib/upload-server/index.ts b/aio/aio-builds-setup/dockerbuild/scripts-js/lib/upload-server/index.ts new file mode 100644 index 0000000000..acfcc98249 --- /dev/null +++ b/aio/aio-builds-setup/dockerbuild/scripts-js/lib/upload-server/index.ts @@ -0,0 +1,42 @@ +// TODO(gkalpak): Find more suitable way to run as `www-data`. +process.setuid('www-data'); + +// Imports +import {GithubPullRequests} from '../common/github-pull-requests'; +import {getEnvVar} from '../common/utils'; +import {CreatedBuildEvent} from './build-events'; +import {uploadServerFactory} from './upload-server-factory'; + +// Constants +const AIO_BUILDS_DIR = getEnvVar('AIO_BUILDS_DIR'); +const AIO_GITHUB_TOKEN = getEnvVar('AIO_GITHUB_TOKEN', true); +const AIO_REPO_SLUG = getEnvVar('AIO_REPO_SLUG', true); +const AIO_UPLOAD_HOSTNAME = getEnvVar('AIO_UPLOAD_HOSTNAME'); +const AIO_UPLOAD_PORT = +getEnvVar('AIO_UPLOAD_PORT'); + +// Run +_main(); + +// Functions +function _main() { + uploadServerFactory. + create(AIO_BUILDS_DIR). + on(CreatedBuildEvent.type, createOnBuildCreatedHanlder()). + listen(AIO_UPLOAD_PORT, AIO_UPLOAD_HOSTNAME); +} + +function createOnBuildCreatedHanlder() { + if (!AIO_REPO_SLUG) { + console.warn('No repo specified. Preview links will not be posted on PRs.'); + return () => null; + } + + const githubPullRequests = new GithubPullRequests(AIO_REPO_SLUG, AIO_GITHUB_TOKEN); + + return ({pr, sha}: CreatedBuildEvent) => { + const body = `The angular.io preview for ${sha.slice(0, 7)} is available [here][1].\n\n` + + `[1]: https://pr${pr}-${sha}.ngbuilds.io/`; + + githubPullRequests.addComment(pr, body); + }; +} diff --git a/aio/aio-builds-setup/dockerbuild/scripts-js/lib/upload-server/upload-error.ts b/aio/aio-builds-setup/dockerbuild/scripts-js/lib/upload-server/upload-error.ts new file mode 100644 index 0000000000..877a3c2baf --- /dev/null +++ b/aio/aio-builds-setup/dockerbuild/scripts-js/lib/upload-server/upload-error.ts @@ -0,0 +1,8 @@ +// Classes +export class UploadError extends Error { + // Constructor + constructor(public status: number = 500, message?: string) { + super(message); + Object.setPrototypeOf(this, UploadError.prototype); + } +} diff --git a/aio/aio-builds-setup/dockerbuild/scripts-js/lib/upload-server/upload-server-factory.ts b/aio/aio-builds-setup/dockerbuild/scripts-js/lib/upload-server/upload-server-factory.ts new file mode 100644 index 0000000000..f0726cc901 --- /dev/null +++ b/aio/aio-builds-setup/dockerbuild/scripts-js/lib/upload-server/upload-server-factory.ts @@ -0,0 +1,76 @@ +// Imports +import * as express from 'express'; +import * as http from 'http'; +import {BuildCreator} from './build-creator'; +import {CreatedBuildEvent} from './build-events'; +import {UploadError} from './upload-error'; + +// Constants +const X_FILE_HEADER = 'X-FILE'; + +// Classes +class UploadServerFactory { + // Methods - Public + public create(buildsDir: string): http.Server { + if (!buildsDir) { + throw new Error('Missing or empty required parameter \'buildsDir\'!'); + } + + const buildCreator = new BuildCreator(buildsDir); + const middleware = this.createMiddleware(buildCreator); + const httpServer = http.createServer(middleware); + + buildCreator.on(CreatedBuildEvent.type, (data: CreatedBuildEvent) => httpServer.emit(CreatedBuildEvent.type, data)); + httpServer.on('listening', () => { + const info = httpServer.address(); + console.info(`Up and running (and listening on ${info.address}:${info.port})...`); + }); + + return httpServer; + } + + // Methods - Protected + protected createMiddleware(buildCreator: BuildCreator): express.Express { + const middleware = express(); + + middleware.get(/^\/create-build\/([1-9][0-9]*)\/([0-9a-f]{40})\/?$/, (req, res) => { + const pr = req.params[0]; + const sha = req.params[1]; + const archive = req.header(X_FILE_HEADER); + + if (!archive) { + this.throwRequestError(400, `Missing or empty '${X_FILE_HEADER}' header`, req); + } + + buildCreator. + create(pr, sha, archive). + then(() => res.sendStatus(201)). + catch(err => this.respondWithError(res, err)); + }); + middleware.get(/^\/health-check\/?$/, (_req, res) => res.sendStatus(200)); + middleware.get('*', req => this.throwRequestError(404, 'Unknown resource', req)); + middleware.all('*', req => this.throwRequestError(405, 'Unsupported method', req)); + middleware.use((err: any, _req: any, res: express.Response, _next: any) => this.respondWithError(res, err)); + + return middleware; + } + + protected respondWithError(res: express.Response, err: any) { + if (!(err instanceof UploadError)) { + err = new UploadError(500, String((err && err.message) || err)); + } + + const statusText = http.STATUS_CODES[err.status] || '???'; + console.error(`Upload error: ${err.status} - ${statusText}`); + console.error(err.message); + + res.status(err.status).end(err.message); + } + + protected throwRequestError(status: number, error: string, req: express.Request) { + throw new UploadError(status, `${error} in request: ${req.method} ${req.originalUrl}`); + } +} + +// Exports +export const uploadServerFactory = new UploadServerFactory(); diff --git a/aio/aio-builds-setup/dockerbuild/scripts-js/lib/verify-setup/helper.ts b/aio/aio-builds-setup/dockerbuild/scripts-js/lib/verify-setup/helper.ts new file mode 100644 index 0000000000..e6d5c166f3 --- /dev/null +++ b/aio/aio-builds-setup/dockerbuild/scripts-js/lib/verify-setup/helper.ts @@ -0,0 +1,197 @@ +// Imports +import * as cp from 'child_process'; +import * as fs from 'fs'; +import * as http from 'http'; +import * as path from 'path'; +import * as shell from 'shelljs'; +import {getEnvVar} from '../common/utils'; + +// Constans +const SERVER_USER = 'www-data'; +const TEST_AIO_BUILDS_DIR = getEnvVar('TEST_AIO_BUILDS_DIR'); +const TEST_AIO_NGINX_HOSTNAME = getEnvVar('TEST_AIO_NGINX_HOSTNAME'); +const TEST_AIO_NGINX_PORT_HTTP = +getEnvVar('TEST_AIO_NGINX_PORT_HTTP'); +const TEST_AIO_NGINX_PORT_HTTPS = +getEnvVar('TEST_AIO_NGINX_PORT_HTTPS'); +const TEST_AIO_UPLOAD_HOSTNAME = getEnvVar('TEST_AIO_UPLOAD_HOSTNAME'); +const TEST_AIO_UPLOAD_MAX_SIZE = +getEnvVar('TEST_AIO_UPLOAD_MAX_SIZE'); +const TEST_AIO_UPLOAD_PORT = +getEnvVar('TEST_AIO_UPLOAD_PORT'); + +// Interfaces - Types +export interface CleanUpFn extends Function {} +export interface CmdResult { success: boolean; err: Error; stdout: string; stderr: string; } +export interface FileSpecs { content?: string; size?: number; } +export interface TestSuiteFactory { (scheme: string, port: number): void } +export interface VerifyCmdResultFn { (result: CmdResult): void; } + +// Classes +class Helper { + // Properties - Public + public get buildsDir() { return TEST_AIO_BUILDS_DIR; } + public get nginxHostname() { return TEST_AIO_NGINX_HOSTNAME; } + public get nginxPortHttp() { return TEST_AIO_NGINX_PORT_HTTP; } + public get nginxPortHttps() { return TEST_AIO_NGINX_PORT_HTTPS; } + public get serverUser() { return SERVER_USER; } + public get uploadHostname() { return TEST_AIO_UPLOAD_HOSTNAME; } + public get uploadPort() { return TEST_AIO_UPLOAD_PORT; } + public get uploadMaxSize() { return TEST_AIO_UPLOAD_MAX_SIZE; } + + // Properties - Protected + protected cleanUpFns: CleanUpFn[] = []; + protected portPerScheme: {[scheme: string]: number} = { + http: this.nginxPortHttp, + https: this.nginxPortHttps, + }; + + // Constructor + constructor() { + shell.mkdir('-p', this.buildsDir); + shell.exec(`chown -R ${this.serverUser} ${this.buildsDir}`); + } + + // Methods - Public + public cleanUp() { + while (this.cleanUpFns.length) { + // Clean-up fns remove themselves from the list. + this.cleanUpFns[0](); + } + + if (fs.readdirSync(this.buildsDir).length) { + throw new Error(`Directory '${this.buildsDir}' is not empty after clean-up.`); + } + } + + public createDummyArchive(pr: string, sha: string, archivePath: string): CleanUpFn { + const inputDir = path.join(this.buildsDir, 'uploaded', pr, sha); + const cmd1 = `tar --create --gzip --directory "${inputDir}" --file "${archivePath}" .`; + const cmd2 = `chown ${this.serverUser} ${archivePath}`; + + const cleanUpTemp = this.createDummyBuild(`uploaded/${pr}`, sha, true); + shell.exec(cmd1); + shell.exec(cmd2); + cleanUpTemp(); + + return this.createCleanUpFn(() => shell.rm('-rf', archivePath)); + } + + public createDummyBuild(pr: string, sha: string, force = false): CleanUpFn { + const prDir = path.join(this.buildsDir, pr); + const shaDir = path.join(prDir, sha); + const idxPath = path.join(shaDir, 'index.html'); + const barPath = path.join(shaDir, 'foo', 'bar.js'); + + this.writeFile(idxPath, {content: `PR: ${pr} | SHA: ${sha} | File: /index.html`}, force); + this.writeFile(barPath, {content: `PR: ${pr} | SHA: ${sha} | File: /foo/bar.js`}, force); + shell.exec(`chown -R ${this.serverUser} ${prDir}`); + + return this.createCleanUpFn(() => shell.rm('-rf', prDir)); + } + + public deletePrDir(pr: string) { + const prDir = path.join(this.buildsDir, pr); + + if (fs.existsSync(prDir)) { + // Undocumented signature (see https://github.com/shelljs/shelljs/pull/663). + (shell as any).chmod('-R', 'a+w', prDir); + shell.rm('-rf', prDir); + } + } + + public readBuildFile(pr: string, sha: string, relFilePath: string): string { + const absFilePath = path.join(this.buildsDir, pr, sha, relFilePath); + return fs.readFileSync(absFilePath, 'utf8'); + } + + public runCmd(cmd: string, opts: cp.ExecFileOptions = {}): Promise { + return new Promise(resolve => { + const proc = cp.exec(cmd, opts, (err, stdout, stderr) => resolve({success: !err, err, stdout, stderr})); + this.createCleanUpFn(() => proc.kill()); + }); + } + + public runForAllSupportedSchemes(suiteFactory: TestSuiteFactory) { + Object.keys(this.portPerScheme).forEach(scheme => { + // TODO (gkalpak): Enable HTTPS tests + if (scheme === 'https') { + return it('should have tests'); + } + + suiteFactory(scheme, this.portPerScheme[scheme]); + }); + } + + public verifyResponse(status: number | (number | string)[], regex = /^/): VerifyCmdResultFn { + let statusCode: number; + let statusText: string; + + if (Array.isArray(status)) { + statusCode = status[0] as number; + statusText = status[1] as string; + } else { + statusCode = status; + statusText = http.STATUS_CODES[statusCode]; + } + + return (result: CmdResult) => { + const [headers, body] = result.stdout. + split(/(?:\r?\n){2,}/). + map(s => s.trim()). + slice(-2); + + if (!result.success) { + console.log('Stdout:', result.stdout); + console.log('Stderr:', result.stderr); + console.log('Error:', result.err); + } + + expect(result.success).toBe(true); + expect(headers).toContain(`${statusCode} ${statusText}`); + expect(body).toMatch(regex); + }; + } + + public writeBuildFile(pr: string, sha: string, relFilePath: string, content: string): CleanUpFn { + const absFilePath = path.join(this.buildsDir, pr, sha, relFilePath); + return this.writeFile(absFilePath, {content}, true); + } + + public writeFile(filePath: string, {content, size}: FileSpecs, force = false): CleanUpFn { + if (!force && fs.existsSync(filePath)) { + throw new Error(`Refusing to overwrite existing file '${filePath}'.`); + } + + let cleanUpTarget = filePath; + while (!fs.existsSync(path.dirname(cleanUpTarget))) { + cleanUpTarget = path.dirname(cleanUpTarget); + } + + shell.mkdir('-p', path.dirname(filePath)); + if (size) { + // Create a file of the specified size. + cp.execSync(`fallocate -l ${size} ${filePath}`); + } else { + // Create a file with the specified content. + fs.writeFileSync(filePath, content || ''); + } + shell.exec(`chown ${this.serverUser} ${filePath}`); + + return this.createCleanUpFn(() => shell.rm('-rf', cleanUpTarget)); + } + + // Methods - Protected + protected createCleanUpFn(fn: Function): CleanUpFn { + const cleanUpFn = () => { + const idx = this.cleanUpFns.indexOf(cleanUpFn); + if (idx !== -1) { + this.cleanUpFns.splice(idx, 1); + fn(); + } + }; + + this.cleanUpFns.push(cleanUpFn); + + return cleanUpFn; + } +} + +// Exports +export const helper = new Helper(); diff --git a/aio/aio-builds-setup/dockerbuild/scripts-js/lib/verify-setup/index.ts b/aio/aio-builds-setup/dockerbuild/scripts-js/lib/verify-setup/index.ts new file mode 100644 index 0000000000..37e6d19a53 --- /dev/null +++ b/aio/aio-builds-setup/dockerbuild/scripts-js/lib/verify-setup/index.ts @@ -0,0 +1,6 @@ +// Imports +import {runTests} from '../common/run-tests'; + +// Run +const specFiles = [`${__dirname}/**/*.e2e.js`]; +runTests(specFiles); diff --git a/aio/aio-builds-setup/dockerbuild/scripts-js/lib/verify-setup/nginx.e2e.ts b/aio/aio-builds-setup/dockerbuild/scripts-js/lib/verify-setup/nginx.e2e.ts new file mode 100644 index 0000000000..bc1b66df40 --- /dev/null +++ b/aio/aio-builds-setup/dockerbuild/scripts-js/lib/verify-setup/nginx.e2e.ts @@ -0,0 +1,232 @@ +// Imports +import * as path from 'path'; +import {helper as h} from './helper'; + +// Tests +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); + + beforeEach(() => jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000); + afterEach(() => h.cleanUp()); + + + describe(`pr-.${host}/*`, () => { + + beforeEach(() => { + h.createDummyBuild(pr, sha9); + h.createDummyBuild(pr, sha0); + }); + + + it('should return /index.html', done => { + const origin = `${scheme}://pr${pr}-${sha9}.${host}`; + const bodyegex = new RegExp(`^PR: ${pr} | SHA: ${sha9} | File: /index\\.html$`); + + Promise.all([ + h.runCmd(`curl -iL ${origin}/index.html`).then(h.verifyResponse(200, bodyegex)), + h.runCmd(`curl -iL ${origin}/`).then(h.verifyResponse(200, bodyegex)), + h.runCmd(`curl -iL ${origin}`).then(h.verifyResponse(200, bodyegex)), + ]).then(done); + }); + + + it('should return /foo/bar.js', done => { + const bodyegex = new RegExp(`^PR: ${pr} | SHA: ${sha9} | File: /foo/bar\\.js$`); + + h.runCmd(`curl -iL ${scheme}://pr${pr}-${sha9}.${host}/foo/bar.js`). + then(h.verifyResponse(200, bodyegex)). + 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', done => { + h.runCmd(`curl -iL ${scheme}://pr${pr}-${sha9}.${host}/foo/baz.css`). + then(h.verifyResponse(404)). + 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 ignore them)', done => { + const bodyegex = 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}-${sha0}.${host}`).then(h.verifyResponse(200, bodyegex)), + ]).then(done); + }); + + }); + + + describe(`${host}/health-check`, () => { + + it('should respond with 200', done => { + Promise.all([ + h.runCmd(`curl -iL ${scheme}://${host}/health-check`).then(h.verifyResponse(200)), + h.runCmd(`curl -iL ${scheme}://${host}/health-check/`).then(h.verifyResponse(200)), + ]).then(done); + }); + + + it('should respond with 404 if the path does not match exactly', done => { + Promise.all([ + h.runCmd(`curl -iL ${scheme}://${host}/health-check/foo`).then(h.verifyResponse(404)), + h.runCmd(`curl -iL ${scheme}://${host}/health-check-foo`).then(h.verifyResponse(404)), + h.runCmd(`curl -iL ${scheme}://${host}/health-checknfoo`).then(h.verifyResponse(404)), + h.runCmd(`curl -iL ${scheme}://${host}/foo/health-check`).then(h.verifyResponse(404)), + h.runCmd(`curl -iL ${scheme}://${host}/foo-health-check`).then(h.verifyResponse(404)), + h.runCmd(`curl -iL ${scheme}://${host}/foonhealth-check`).then(h.verifyResponse(404)), + ]).then(done); + }); + + }); + + + describe(`${host}/create-build//`, () => { + + it('should disallow non-POST requests', done => { + const url = `${scheme}://${host}/create-build/${pr}/${sha9}`; + + Promise.all([ + h.runCmd(`curl -iLX GET ${url}`).then(h.verifyResponse([405, 'Not Allowed'])), + h.runCmd(`curl -iLX PUT ${url}`).then(h.verifyResponse([405, 'Not Allowed'])), + h.runCmd(`curl -iLX PATCH ${url}`).then(h.verifyResponse([405, 'Not Allowed'])), + h.runCmd(`curl -iLX DELETE ${url}`).then(h.verifyResponse([405, 'Not Allowed'])), + ]).then(done); + }); + + + it(`should reject files larger than ${h.uploadMaxSize}B (according to header)`, done => { + const headers = `--header "Content-Length: ${1.5 * h.uploadMaxSize}"`; + const url = `${scheme}://${host}/create-build/${pr}/${sha9}`; + + h.runCmd(`curl -iLX POST ${headers} ${url}`). + then(h.verifyResponse([413, 'Request Entity Too Large'])). + then(done); + }); + + + it(`should reject files larger than ${h.uploadMaxSize}B (without header)`, done => { + const filePath = path.join(h.buildsDir, 'snapshot.tar.gz'); + const url = `${scheme}://${host}/create-build/${pr}/${sha9}`; + + h.writeFile(filePath, {size: 1.5 * h.uploadMaxSize}); + + h.runCmd(`curl -iLX POST --data-binary "@${filePath}" ${url}`). + then(h.verifyResponse([413, 'Request Entity Too Large'])). + then(done); + }); + + + it('should pass requests through to the upload server', done => { + h.runCmd(`curl -iLX POST ${scheme}://${host}/create-build/${pr}/${sha9}`). + then(h.verifyResponse(400, /Missing or empty 'X-FILE' header/)). + then(done); + }); + + + it('should respond with 404 for unknown paths', done => { + const cmdPrefix = `curl -iLX POST ${scheme}://${host}`; + + Promise.all([ + h.runCmd(`${cmdPrefix}/foo/create-build/${pr}/${sha9}`).then(h.verifyResponse(404)), + h.runCmd(`${cmdPrefix}/foo-create-build/${pr}/${sha9}`).then(h.verifyResponse(404)), + h.runCmd(`${cmdPrefix}/fooncreate-build/${pr}/${sha9}`).then(h.verifyResponse(404)), + h.runCmd(`${cmdPrefix}/create-build/foo/${pr}/${sha9}`).then(h.verifyResponse(404)), + h.runCmd(`${cmdPrefix}/create-build-foo/${pr}/${sha9}`).then(h.verifyResponse(404)), + h.runCmd(`${cmdPrefix}/create-buildnfoo/${pr}/${sha9}`).then(h.verifyResponse(404)), + h.runCmd(`${cmdPrefix}/create-build/pr${pr}/${sha9}`).then(h.verifyResponse(404)), + h.runCmd(`${cmdPrefix}/create-build/${pr}/${sha9}42`).then(h.verifyResponse(404)), + ]).then(done); + }); + + + it('should reject PRs with leading zeros', done => { + h.runCmd(`curl -iLX POST ${scheme}://${host}/create-build/0${pr}/${sha9}`). + then(h.verifyResponse(404)). + then(done); + }); + + + it('should accept SHAs with leading zeros (but not ignore them)', done => { + const cmdPrefix = `curl -iLX POST ${scheme}://${host}/create-build/${pr}`; + const bodyRegex = /Missing or empty 'X-FILE' header/; + + Promise.all([ + h.runCmd(`${cmdPrefix}/0${sha9}`).then(h.verifyResponse(404)), + h.runCmd(`${cmdPrefix}/${sha0}`).then(h.verifyResponse(400, bodyRegex)), + ]).then(done); + }); + + }); + + + describe(`${host}/*`, () => { + + it('should respond with 404 for unkown URLs (even if the resource exists)', done => { + ['index.html', 'foo.js', 'foo/index.html'].forEach(relFilePath => { + const absFilePath = path.join(h.buildsDir, relFilePath); + h.writeFile(absFilePath, {content: `File: /${relFilePath}`}); + }); + + Promise.all([ + h.runCmd(`curl -iL ${scheme}://${host}/index.html`).then(h.verifyResponse(404)), + h.runCmd(`curl -iL ${scheme}://${host}/`).then(h.verifyResponse(404)), + h.runCmd(`curl -iL ${scheme}://${host}`).then(h.verifyResponse(404)), + h.runCmd(`curl -iL ${scheme}://foo.${host}/index.html`).then(h.verifyResponse(404)), + h.runCmd(`curl -iL ${scheme}://foo.${host}/`).then(h.verifyResponse(404)), + h.runCmd(`curl -iL ${scheme}://foo.${host}`).then(h.verifyResponse(404)), + h.runCmd(`curl -iL ${scheme}://${host}/foo.js`).then(h.verifyResponse(404)), + h.runCmd(`curl -iL ${scheme}://${host}/foo/index.html`).then(h.verifyResponse(404)), + ]).then(done); + }); + + }); + +})); diff --git a/aio/aio-builds-setup/dockerbuild/scripts-js/lib/verify-setup/server-integration.e2e.ts b/aio/aio-builds-setup/dockerbuild/scripts-js/lib/verify-setup/server-integration.e2e.ts new file mode 100644 index 0000000000..e368a4c752 --- /dev/null +++ b/aio/aio-builds-setup/dockerbuild/scripts-js/lib/verify-setup/server-integration.e2e.ts @@ -0,0 +1,82 @@ +// Imports +import * as path from 'path'; +import {helper as h} from './helper'; + +// Tests +h.runForAllSupportedSchemes((scheme, port) => describe(`integration (on ${scheme.toUpperCase()})`, () => { + const hostname = h.nginxHostname; + const host = `${hostname}:${port}`; + const pr9 = '9'; + const sha9 = '9'.repeat(40); + const sha0 = '0'.repeat(40); + const archivePath = path.join(h.buildsDir, 'snapshot.tar.gz'); + + const getFile = (pr: string, sha: string, file: string) => + h.runCmd(`curl -iL ${scheme}://pr${pr}-${sha}.${host}/${file}`); + const uploadBuild = (pr: string, sha: string, archive: string) => + h.runCmd(`curl -iLX POST --data-binary "@${archive}" ${scheme}://${host}/create-build/${pr}/${sha}`); + + beforeEach(() => jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000); + afterEach(() => { + h.deletePrDir(pr9); + h.cleanUp(); + }); + + + it('should be able to upload and serve a build for a new PR', done => { + const regexPrefix9 = `^PR: uploaded\\/${pr9} \\| SHA: ${sha9} \\| File:`; + const idxContentRegex9 = new RegExp(`${regexPrefix9} \\/index\\.html$`); + const barContentRegex9 = new RegExp(`${regexPrefix9} \\/foo\\/bar\\.js$`); + + h.createDummyArchive(pr9, sha9, archivePath); + + uploadBuild(pr9, sha9, archivePath). + then(() => Promise.all([ + getFile(pr9, sha9, 'index.html').then(h.verifyResponse(200, idxContentRegex9)), + getFile(pr9, sha9, 'foo/bar.js').then(h.verifyResponse(200, barContentRegex9)), + ])). + then(done); + }); + + + it('should be able to upload and serve a build for an existing PR', done => { + const regexPrefix0 = `^PR: ${pr9} \\| SHA: ${sha0} \\| File:`; + const idxContentRegex0 = new RegExp(`${regexPrefix0} \\/index\\.html$`); + const barContentRegex0 = new RegExp(`${regexPrefix0} \\/foo\\/bar\\.js$`); + + const regexPrefix9 = `^PR: uploaded\\/${pr9} \\| SHA: ${sha9} \\| File:`; + const idxContentRegex9 = new RegExp(`${regexPrefix9} \\/index\\.html$`); + const barContentRegex9 = new RegExp(`${regexPrefix9} \\/foo\\/bar\\.js$`); + + h.createDummyBuild(pr9, sha0); + h.createDummyArchive(pr9, sha9, archivePath); + + uploadBuild(pr9, sha9, archivePath). + then(() => Promise.all([ + getFile(pr9, sha0, 'index.html').then(h.verifyResponse(200, idxContentRegex0)), + getFile(pr9, sha0, 'foo/bar.js').then(h.verifyResponse(200, barContentRegex0)), + getFile(pr9, sha9, 'index.html').then(h.verifyResponse(200, idxContentRegex9)), + getFile(pr9, sha9, 'foo/bar.js').then(h.verifyResponse(200, barContentRegex9)), + ])). + then(done); + }); + + + it('should not be able to overwrite a build', done => { + const regexPrefix9 = `^PR: ${pr9} \\| SHA: ${sha9} \\| File:`; + const idxContentRegex9 = new RegExp(`${regexPrefix9} \\/index\\.html$`); + const barContentRegex9 = new RegExp(`${regexPrefix9} \\/foo\\/bar\\.js$`); + + h.createDummyBuild(pr9, sha9); + h.createDummyArchive(pr9, sha9, archivePath); + + uploadBuild(pr9, sha9, archivePath). + then(h.verifyResponse(403)). + then(() => Promise.all([ + getFile(pr9, sha9, 'index.html').then(h.verifyResponse(200, idxContentRegex9)), + getFile(pr9, sha9, 'foo/bar.js').then(h.verifyResponse(200, barContentRegex9)), + ])). + then(done); + }); + +})); diff --git a/aio/aio-builds-setup/dockerbuild/scripts-js/lib/verify-setup/upload-server.e2e.ts b/aio/aio-builds-setup/dockerbuild/scripts-js/lib/verify-setup/upload-server.e2e.ts new file mode 100644 index 0000000000..244e9670ca --- /dev/null +++ b/aio/aio-builds-setup/dockerbuild/scripts-js/lib/verify-setup/upload-server.e2e.ts @@ -0,0 +1,249 @@ +// Imports +import * as fs from 'fs'; +import * as path from 'path'; +import {CmdResult, helper as h} from './helper'; + +// Tests +describe('upload-server (on HTTP)', () => { + const hostname = h.uploadHostname; + const port = h.uploadPort; + const host = `${hostname}:${port}`; + const pr = '9'; + const sha9 = '9'.repeat(40); + const sha0 = '0'.repeat(40); + + beforeEach(() => jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000); + afterEach(() => h.cleanUp()); + + + describe(`${host}/create-build//`, () => { + const curl = `curl -iL --header "X-FILE: ${h.buildsDir}/snapshot.tar.gz"`; + + + it('should disallow non-GET requests', done => { + const url = `http://${host}/create-build/${pr}/${sha9}`; + const bodyRegex = /^Unsupported method/; + + Promise.all([ + h.runCmd(`curl -iLX PUT ${url}`).then(h.verifyResponse(405, bodyRegex)), + h.runCmd(`curl -iLX POST ${url}`).then(h.verifyResponse(405, bodyRegex)), + h.runCmd(`curl -iLX PATCH ${url}`).then(h.verifyResponse(405, bodyRegex)), + h.runCmd(`curl -iLX DELETE ${url}`).then(h.verifyResponse(405, bodyRegex)), + ]).then(done); + }); + + + it('should reject requests without an \'X-FILE\' header', done => { + const headers = '--header "X-FILE: "'; + const url = `http://${host}/create-build/${pr}/${sha9}`; + const bodyRegex = /^Missing or empty 'X-FILE' header/; + + Promise.all([ + h.runCmd(`curl -iL ${url}`).then(h.verifyResponse(400, bodyRegex)), + h.runCmd(`curl -iL ${headers} ${url}`).then(h.verifyResponse(400, bodyRegex)), + ]).then(done); + }); + + + it('should respond with 404 for unknown paths', done => { + const cmdPrefix = `${curl} http://${host}`; + + Promise.all([ + h.runCmd(`${cmdPrefix}/foo/create-build/${pr}/${sha9}`).then(h.verifyResponse(404)), + h.runCmd(`${cmdPrefix}/foo-create-build/${pr}/${sha9}`).then(h.verifyResponse(404)), + h.runCmd(`${cmdPrefix}/fooncreate-build/${pr}/${sha9}`).then(h.verifyResponse(404)), + h.runCmd(`${cmdPrefix}/create-build/foo/${pr}/${sha9}`).then(h.verifyResponse(404)), + h.runCmd(`${cmdPrefix}/create-build-foo/${pr}/${sha9}`).then(h.verifyResponse(404)), + h.runCmd(`${cmdPrefix}/create-buildnfoo/${pr}/${sha9}`).then(h.verifyResponse(404)), + h.runCmd(`${cmdPrefix}/create-build/pr${pr}/${sha9}`).then(h.verifyResponse(404)), + h.runCmd(`${cmdPrefix}/create-build/${pr}/${sha9}42`).then(h.verifyResponse(404)), + ]).then(done); + }); + + + it('should reject PRs with leading zeros', done => { + h.runCmd(`${curl} http://${host}/create-build/0${pr}/${sha9}`). + then(h.verifyResponse(404)). + then(done); + }); + + + it('should accept SHAs with leading zeros (but not ignore them)', 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}/${sha0}`).then(h.verifyResponse(500)), + ]).then(done); + }); + + + it('should not overwrite existing builds', done => { + h.createDummyBuild(pr, sha9); + expect(h.readBuildFile(pr, sha9, 'index.html')).toContain('index.html'); + + h.writeBuildFile(pr, sha9, 'index.html', 'My content'); + expect(h.readBuildFile(pr, sha9, 'index.html')).toBe('My content'); + + h.runCmd(`${curl} http://${host}/create-build/${pr}/${sha9}`). + then(h.verifyResponse(403, /^Request to overwrite existing directory/)). + then(() => expect(h.readBuildFile(pr, sha9, 'index.html')).toBe('My content')). + then(done); + }); + + + it('should delete the PR directory on error (for new PR)', done => { + const prDir = path.join(h.buildsDir, pr); + + h.runCmd(`${curl} http://${host}/create-build/${pr}/${sha9}`). + then(h.verifyResponse(500)). + then(() => expect(fs.existsSync(prDir)).toBe(false)). + then(done); + }); + + + it('should only delete the SHA directory on error (for existing PR)', done => { + const prDir = path.join(h.buildsDir, pr); + const shaDir = path.join(prDir, sha9); + + h.createDummyBuild(pr, sha0); + + h.runCmd(`${curl} http://${host}/create-build/${pr}/${sha9}`). + then(h.verifyResponse(500)). + then(() => { + expect(fs.existsSync(shaDir)).toBe(false); + expect(fs.existsSync(prDir)).toBe(true); + }). + then(done); + }); + + + describe('on successful upload', () => { + const archivePath = path.join(h.buildsDir, 'snapshot.tar.gz'); + let uploadPromise: Promise; + + beforeEach(() => { + h.createDummyArchive(pr, sha9, archivePath); + uploadPromise = h.runCmd(`${curl} http://${host}/create-build/${pr}/${sha9}`); + }); + afterEach(() => h.deletePrDir(pr)); + + + it('should respond with 201', done => { + uploadPromise.then(h.verifyResponse(201)).then(done); + }); + + + it('should extract the contents of the uploaded file', done => { + uploadPromise. + then(() => { + expect(h.readBuildFile(pr, sha9, 'index.html')).toContain(`uploaded/${pr}`); + expect(h.readBuildFile(pr, sha9, 'foo/bar.js')).toContain(`uploaded/${pr}`); + }). + then(done); + }); + + + it(`should create files/directories owned by '${h.serverUser}'`, done => { + const shaDir = path.join(h.buildsDir, pr, sha9); + const idxPath = path.join(shaDir, 'index.html'); + const barPath = path.join(shaDir, 'foo', 'bar.js'); + + uploadPromise. + then(() => Promise.all([ + h.runCmd(`find ${shaDir}`), + h.runCmd(`find ${shaDir} -user ${h.serverUser}`), + ])). + then(([{stdout: allFiles}, {stdout: userFiles}]) => { + expect(userFiles).toBe(allFiles); + expect(userFiles).toContain(shaDir); + expect(userFiles).toContain(idxPath); + expect(userFiles).toContain(barPath); + }). + then(done); + }); + + + it('should delete the uploaded file', done => { + expect(fs.existsSync(archivePath)).toBe(true); + uploadPromise. + then(() => expect(fs.existsSync(archivePath)).toBe(false)). + then(done); + }); + + + it('should make the build directory non-writable', done => { + const shaDir = path.join(h.buildsDir, pr, sha9); + const idxPath = path.join(shaDir, 'index.html'); + const barPath = path.join(shaDir, 'foo', 'bar.js'); + + // See https://github.com/nodejs/node-v0.x-archive/issues/3045#issuecomment-4862588. + const isNotWritable = (fileOrDir: string) => { + const mode = fs.statSync(fileOrDir).mode; + // tslint:disable-next-line: no-bitwise + return !(mode & parseInt('222', 8)); + }; + + uploadPromise. + then(() => { + expect(isNotWritable(shaDir)).toBe(true); + expect(isNotWritable(idxPath)).toBe(true); + expect(isNotWritable(barPath)).toBe(true); + }). + then(done); + }); + + }); + + }); + + + describe(`${host}/health-check`, () => { + + it('should respond with 200', done => { + Promise.all([ + h.runCmd(`curl -iL http://${host}/health-check`).then(h.verifyResponse(200)), + h.runCmd(`curl -iL http://${host}/health-check/`).then(h.verifyResponse(200)), + ]).then(done); + }); + + + it('should respond with 404 if the path does not match exactly', done => { + Promise.all([ + h.runCmd(`curl -iL http://${host}/health-check/foo`).then(h.verifyResponse(404)), + h.runCmd(`curl -iL http://${host}/health-check-foo`).then(h.verifyResponse(404)), + h.runCmd(`curl -iL http://${host}/health-checknfoo`).then(h.verifyResponse(404)), + h.runCmd(`curl -iL http://${host}/foo/health-check`).then(h.verifyResponse(404)), + h.runCmd(`curl -iL http://${host}/foo-health-check`).then(h.verifyResponse(404)), + h.runCmd(`curl -iL http://${host}/foonhealth-check`).then(h.verifyResponse(404)), + ]).then(done); + }); + + }); + + + describe(`${host}/*`, () => { + + it('should respond with 404 for GET requests to unknown URLs', done => { + const bodyRegex = /^Unknown resource/; + + Promise.all([ + h.runCmd(`curl -iL http://${host}/index.html`).then(h.verifyResponse(404, bodyRegex)), + h.runCmd(`curl -iL http://${host}/`).then(h.verifyResponse(404, bodyRegex)), + h.runCmd(`curl -iL http://${host}`).then(h.verifyResponse(404, bodyRegex)), + ]).then(done); + }); + + + it('should respond with 405 for non-GET requests to any URL', done => { + const bodyRegex = /^Unsupported method/; + + Promise.all([ + h.runCmd(`curl -iLX PUT http://${host}`).then(h.verifyResponse(405, bodyRegex)), + h.runCmd(`curl -iLX POST http://${host}`).then(h.verifyResponse(405, bodyRegex)), + h.runCmd(`curl -iLX PATCH http://${host}`).then(h.verifyResponse(405, bodyRegex)), + h.runCmd(`curl -iLX DELETE http://${host}`).then(h.verifyResponse(405, bodyRegex)), + ]).then(done); + }); + + }); + +}); diff --git a/aio/aio-builds-setup/dockerbuild/scripts-js/package.json b/aio/aio-builds-setup/dockerbuild/scripts-js/package.json new file mode 100644 index 0000000000..c7c3a34bfc --- /dev/null +++ b/aio/aio-builds-setup/dockerbuild/scripts-js/package.json @@ -0,0 +1,38 @@ +{ + "name": "aio-scripts-js", + "version": "1.0.0", + "description": "Performing various tasks on PR build artifacts for angular.io.", + "repository": "https://github.com/angular/angular.git", + "author": "Angular", + "license": "MIT", + "scripts": { + "prebuild": "yarn run clean", + "build": "tsc", + "build-watch": "yarn run tsc -- --watch", + "clean": "node --eval \"require('shelljs').rm('-rf', 'dist')\"", + "dev": "concurrently --kill-others --raw --success first \"yarn run build-watch\" \"yarn run test-watch\"", + "lint": "tslint --project tsconfig.json", + "pretest": "yarn run lint", + "test": "node dist/test", + "test-watch": "nodemon --exec \"yarn test\" --watch dist" + }, + "dependencies": { + "express": "^4.14.1", + "jasmine": "^2.5.3", + "shelljs": "^0.7.6" + }, + "devDependencies": { + "@types/express": "^4.0.35", + "@types/jasmine": "^2.5.43", + "@types/node": "^7.0.5", + "@types/shelljs": "^0.7.0", + "@types/supertest": "^2.0.0", + "concurrently": "^3.3.0", + "eslint": "^3.15.0", + "eslint-plugin-jasmine": "^2.2.0", + "nodemon": "^1.11.0", + "supertest": "^3.0.0", + "tslint": "^4.4.2", + "typescript": "^2.1.6" + } +} diff --git a/aio/aio-builds-setup/dockerbuild/scripts-js/test/clean-up/build-cleaner.spec.ts b/aio/aio-builds-setup/dockerbuild/scripts-js/test/clean-up/build-cleaner.spec.ts new file mode 100644 index 0000000000..844559dcb0 --- /dev/null +++ b/aio/aio-builds-setup/dockerbuild/scripts-js/test/clean-up/build-cleaner.spec.ts @@ -0,0 +1,310 @@ +// Imports +import * as fs from 'fs'; +import * as shell from 'shelljs'; +import {BuildCleaner} from '../../lib/clean-up/build-cleaner'; +import {GithubPullRequests} from '../../lib/common/github-pull-requests'; + +// Tests +describe('BuildCleaner', () => { + let cleaner: BuildCleaner; + + beforeEach(() => cleaner = new BuildCleaner('/foo/bar', 'baz/qux', '12345')); + + + describe('constructor()', () => { + + it('should throw if \'buildsDir\' is empty', () => { + expect(() => new BuildCleaner('', '/baz/qux')).toThrowError('Missing required parameter \'buildsDir\'!'); + }); + + + it('should throw if \'repoSlag\' is empty', () => { + expect(() => new BuildCleaner('/foo/bar', '')).toThrowError('Missing required parameter \'repoSlug\'!'); + }); + + }); + + + describe('cleanUp()', () => { + let cleanerGetExistingBuildNumbersSpy: jasmine.Spy; + let cleanerGetOpenPrNumbersSpy: jasmine.Spy; + let cleanerRemoveUnnecessaryBuildsSpy: jasmine.Spy; + let existingBuildsDeferred: {resolve: Function, reject: Function}; + let openPrsDeferred: {resolve: Function, reject: Function}; + let promise: Promise; + + beforeEach(() => { + cleanerGetExistingBuildNumbersSpy = spyOn(cleaner as any, 'getExistingBuildNumbers').and.callFake(() => { + return new Promise((resolve, reject) => existingBuildsDeferred = {resolve, reject}); + }); + cleanerGetOpenPrNumbersSpy = spyOn(cleaner as any, 'getOpenPrNumbers').and.callFake(() => { + return new Promise((resolve, reject) => openPrsDeferred = {resolve, reject}); + }); + cleanerRemoveUnnecessaryBuildsSpy = spyOn(cleaner as any, 'removeUnnecessaryBuilds'); + + promise = cleaner.cleanUp(); + }); + + + it('should return a promise', () => { + expect(promise).toEqual(jasmine.any(Promise)); + }); + + + it('should get the existing builds', () => { + expect(cleanerGetExistingBuildNumbersSpy).toHaveBeenCalled(); + }); + + + it('should get the open PRs', () => { + expect(cleanerGetOpenPrNumbersSpy).toHaveBeenCalled(); + }); + + + it('should reject if \'getExistingBuildNumbers()\' rejects', done => { + promise.catch(err => { + expect(err).toBe('Test'); + done(); + }); + + existingBuildsDeferred.reject('Test'); + }); + + + it('should reject if \'getOpenPrNumbers()\' rejects', done => { + promise.catch(err => { + expect(err).toBe('Test'); + done(); + }); + + openPrsDeferred.reject('Test'); + }); + + + it('should reject if \'removeUnnecessaryBuilds()\' rejects', done => { + promise.catch(err => { + expect(err).toBe('Test'); + done(); + }); + + cleanerRemoveUnnecessaryBuildsSpy.and.returnValue(Promise.reject('Test')); + existingBuildsDeferred.resolve(); + openPrsDeferred.resolve(); + }); + + + it('should pass existing builds and open PRs to \'removeUnnecessaryBuilds()\'', done => { + promise.then(() => { + expect(cleanerRemoveUnnecessaryBuildsSpy).toHaveBeenCalledWith('foo', 'bar'); + done(); + }); + + existingBuildsDeferred.resolve('foo'); + openPrsDeferred.resolve('bar'); + }); + + + it('should resolve with the value returned by \'removeUnnecessaryBuilds()\'', done => { + promise.then(result => { + expect(result).toBe('Test'); + done(); + }); + + cleanerRemoveUnnecessaryBuildsSpy.and.returnValue(Promise.resolve('Test')); + existingBuildsDeferred.resolve(); + openPrsDeferred.resolve(); + }); + + }); + + + // Protected methods + + describe('getExistingBuildNumbers()', () => { + let fsReaddirSpy: jasmine.Spy; + let readdirCb: (err: any, files?: string[]) => void; + let promise: Promise; + + beforeEach(() => { + fsReaddirSpy = spyOn(fs, 'readdir').and.callFake((_: string, cb: typeof readdirCb) => readdirCb = cb); + promise = (cleaner as any).getExistingBuildNumbers(); + }); + + + it('should return a promise', () => { + expect(promise).toEqual(jasmine.any(Promise)); + }); + + + it('should get the contents of the builds directory', () => { + expect(fsReaddirSpy).toHaveBeenCalled(); + expect(fsReaddirSpy.calls.argsFor(0)[0]).toBe('/foo/bar'); + }); + + + it('should reject if an error occurs while getting the files', done => { + promise.catch(err => { + expect(err).toBe('Test'); + done(); + }); + + readdirCb('Test'); + }); + + + it('should resolve with the returned files (as numbers)', done => { + promise.then(result => { + expect(result).toEqual([12, 34, 56]); + done(); + }); + + readdirCb(null, ['12', '34', '56']); + }); + + + it('should ignore files with non-numeric (or zero) names', done => { + promise.then(result => { + expect(result).toEqual([12, 34, 56]); + done(); + }); + + readdirCb(null, ['12', 'foo', '34', 'bar', '56', '000']); + }); + + }); + + + describe('getOpenPrNumbers()', () => { + let prDeferred: {resolve: Function, reject: Function}; + let promise: Promise; + + beforeEach(() => { + spyOn(GithubPullRequests.prototype, 'fetchAll').and.callFake(() => { + return new Promise((resolve, reject) => prDeferred = {resolve, reject}); + }); + + promise = (cleaner as any).getOpenPrNumbers(); + }); + + + it('should return a promise', () => { + expect(promise).toEqual(jasmine.any(Promise)); + }); + + + it('should fetch open PRs via \'GithubPullRequests\'', () => { + expect(GithubPullRequests.prototype.fetchAll).toHaveBeenCalledWith('open'); + }); + + + it('should reject if an error occurs while fetching PRs', done => { + promise.catch(err => { + expect(err).toBe('Test'); + done(); + }); + + prDeferred.reject('Test'); + }); + + + it('should resolve with the numbers of the fetched PRs', done => { + promise.then(prNumbers => { + expect(prNumbers).toEqual([1, 2, 3]); + done(); + }); + + prDeferred.resolve([{id: 0, number: 1}, {id: 1, number: 2}, {id: 2, number: 3}]); + }); + + }); + + + describe('removeDir()', () => { + let shellChmodSpy: jasmine.Spy; + let shellRmSpy: jasmine.Spy; + + beforeEach(() => { + shellChmodSpy = spyOn(shell, 'chmod'); + shellRmSpy = spyOn(shell, 'rm'); + }); + + + it('should remove the specified directory and its content', () => { + (cleaner as any).removeDir('/foo/bar'); + expect(shellRmSpy).toHaveBeenCalledWith('-rf', '/foo/bar'); + }); + + + it('should make the directory and its content writable before removing', () => { + shellRmSpy.and.callFake(() => expect(shellChmodSpy).toHaveBeenCalledWith('-R', 'a+w', '/foo/bar')); + (cleaner as any).removeDir('/foo/bar'); + + expect(shellRmSpy).toHaveBeenCalled(); + }); + + + it('should catch errors and log them', () => { + const consoleErrorSpy = spyOn(console, 'error'); + shellRmSpy.and.callFake(() => { throw 'Test'; }); + + (cleaner as any).removeDir('/foo/bar'); + + expect(consoleErrorSpy).toHaveBeenCalled(); + expect(consoleErrorSpy.calls.argsFor(0)[0]).toContain('Unable to remove \'/foo/bar\''); + expect(consoleErrorSpy.calls.argsFor(0)[1]).toBe('Test'); + }); + + }); + + + describe('removeUnnecessaryBuilds()', () => { + let consoleLogSpy: jasmine.Spy; + let cleanerRemoveDirSpy: jasmine.Spy; + + beforeEach(() => { + consoleLogSpy = spyOn(console, 'log'); + cleanerRemoveDirSpy = spyOn(cleaner as any, 'removeDir'); + }); + + + it('should log the number of existing builds, open PRs and builds to be removed', () => { + (cleaner as any).removeUnnecessaryBuilds([1, 2, 3], [3, 4, 5, 6]); + + expect(console.log).toHaveBeenCalledWith('Existing builds: 3'); + expect(console.log).toHaveBeenCalledWith('Open pull requests: 4'); + expect(console.log).toHaveBeenCalledWith('Removing 2 build(s): 1, 2'); + }); + + + it('should construct full paths to directories (by prepending \'buildsDir\')', () => { + (cleaner as any).removeUnnecessaryBuilds([1, 2, 3], []); + + expect(cleanerRemoveDirSpy).toHaveBeenCalledWith('/foo/bar/1'); + expect(cleanerRemoveDirSpy).toHaveBeenCalledWith('/foo/bar/2'); + expect(cleanerRemoveDirSpy).toHaveBeenCalledWith('/foo/bar/3'); + }); + + + it('should remove the builds that do not correspond to open PRs', () => { + (cleaner as any).removeUnnecessaryBuilds([1, 2, 3, 4], [2, 4]); + expect(cleanerRemoveDirSpy).toHaveBeenCalledTimes(2); + expect(cleanerRemoveDirSpy).toHaveBeenCalledWith('/foo/bar/1'); + expect(cleanerRemoveDirSpy).toHaveBeenCalledWith('/foo/bar/3'); + cleanerRemoveDirSpy.calls.reset(); + + (cleaner as any).removeUnnecessaryBuilds([1, 2, 3, 4], [1, 2, 3, 4]); + expect(cleanerRemoveDirSpy).toHaveBeenCalledTimes(0); + cleanerRemoveDirSpy.calls.reset(); + + (cleaner as any).removeUnnecessaryBuilds([1, 2, 3, 4], []); + expect(cleanerRemoveDirSpy).toHaveBeenCalledTimes(4); + expect(cleanerRemoveDirSpy).toHaveBeenCalledWith('/foo/bar/1'); + expect(cleanerRemoveDirSpy).toHaveBeenCalledWith('/foo/bar/2'); + expect(cleanerRemoveDirSpy).toHaveBeenCalledWith('/foo/bar/3'); + expect(cleanerRemoveDirSpy).toHaveBeenCalledWith('/foo/bar/4'); + cleanerRemoveDirSpy.calls.reset(); + }); + + }); + +}); diff --git a/aio/aio-builds-setup/dockerbuild/scripts-js/test/common/github-api.spec.ts b/aio/aio-builds-setup/dockerbuild/scripts-js/test/common/github-api.spec.ts new file mode 100644 index 0000000000..2dc0a7e505 --- /dev/null +++ b/aio/aio-builds-setup/dockerbuild/scripts-js/test/common/github-api.spec.ts @@ -0,0 +1,350 @@ +// Imports +import {EventEmitter} from 'events'; +import {ClientRequest, IncomingMessage} from 'http'; +import * as https from 'https'; +import {GithubApi} from '../../lib/common/github-api'; + +// Tests +describe('GithubApi', () => { + let api: GithubApi; + + beforeEach(() => api = new GithubApi('repo/slug', '12345')); + + + describe('constructor()', () => { + + it('should throw if \'repoSlag\' is not defined', () => { + expect(() => new GithubApi('', '12345')).toThrowError('Missing required parameter \'repoSlug\'!'); + }); + + + it('should log a warning if \'githubToken\' is not defined or empty', () => { + const warningMessage = 'No GitHub access-token specified. Requests will be unauthenticated.'; + const consoleWarnSpy = spyOn(console, 'warn'); + + /* tslint:disable: no-unused-new */ + new GithubApi('repo/slug'); + new GithubApi('repo/slug', ''); + /* tslint:enable: no-unused-new */ + + expect(consoleWarnSpy).toHaveBeenCalledTimes(2); + expect(consoleWarnSpy.calls.argsFor(0)[0]).toBe(warningMessage); + expect(consoleWarnSpy.calls.argsFor(1)[0]).toBe(warningMessage); + }); + + }); + + + describe('get()', () => { + let apiBuildPathSpy: jasmine.Spy; + let apiRequestSpy: jasmine.Spy; + + beforeEach(() => { + apiBuildPathSpy = spyOn(api as any, 'buildPath'); + apiRequestSpy = spyOn(api as any, 'request'); + }); + + + it('should call \'buildPath()\' with the pathname and params', () => { + api.get('/foo', {bar: 'baz'}); + + expect(apiBuildPathSpy).toHaveBeenCalled(); + expect(apiBuildPathSpy.calls.argsFor(0)).toEqual(['/foo', {bar: 'baz'}]); + }); + + + it('should call \'request()\' with the correct method', () => { + api.get('/foo'); + + expect(apiRequestSpy).toHaveBeenCalled(); + expect(apiRequestSpy.calls.argsFor(0)[0]).toBe('get'); + }); + + + it('should call \'request()\' with the correct path', () => { + apiBuildPathSpy.and.returnValue('/foo/bar'); + api.get('foo'); + + expect(apiRequestSpy).toHaveBeenCalled(); + expect(apiRequestSpy.calls.argsFor(0)[1]).toBe('/foo/bar'); + }); + + + it('should not pass data to \'request()\'', () => { + (api.get as Function)('foo', {}, {}); + + expect(apiRequestSpy).toHaveBeenCalled(); + expect(apiRequestSpy.calls.argsFor(0)[2]).toBeUndefined(); + }); + + }); + + + describe('post()', () => { + let apiBuildPathSpy: jasmine.Spy; + let apiRequestSpy: jasmine.Spy; + + beforeEach(() => { + apiBuildPathSpy = spyOn(api as any, 'buildPath'); + apiRequestSpy = spyOn(api as any, 'request'); + }); + + + it('should call \'buildPath()\' with the pathname and params', () => { + api.post('/foo', {bar: 'baz'}); + + expect(apiBuildPathSpy).toHaveBeenCalled(); + expect(apiBuildPathSpy.calls.argsFor(0)).toEqual(['/foo', {bar: 'baz'}]); + }); + + + it('should call \'request()\' with the correct method', () => { + api.post('/foo'); + + expect(apiRequestSpy).toHaveBeenCalled(); + expect(apiRequestSpy.calls.argsFor(0)[0]).toBe('post'); + }); + + + it('should call \'request()\' with the correct path', () => { + apiBuildPathSpy.and.returnValue('/foo/bar'); + api.post('/foo'); + + expect(apiRequestSpy).toHaveBeenCalled(); + expect(apiRequestSpy.calls.argsFor(0)[1]).toBe('/foo/bar'); + }); + + + it('should pass the data to \'request()\'', () => { + api.post('/foo', {}, {bar: 'baz'}); + + expect(apiRequestSpy).toHaveBeenCalled(); + expect(apiRequestSpy.calls.argsFor(0)[2]).toEqual({bar: 'baz'}); + }); + + }); + + + // Protected methods + + describe('buildPath()', () => { + + it('should return the pathname if no params', () => { + expect((api as any).buildPath('/foo')).toBe('/foo'); + expect((api as any).buildPath('/foo', undefined)).toBe('/foo'); + expect((api as any).buildPath('/foo', null)).toBe('/foo'); + }); + + + it('should append the params to the pathname', () => { + expect((api as any).buildPath('/foo', {bar: 'baz'})).toBe('/foo?bar=baz'); + }); + + + it('should join the params with \'&\'', () => { + expect((api as any).buildPath('/foo', {bar: 1, baz: 2})).toBe('/foo?bar=1&baz=2'); + }); + + + it('should ignore undefined/null params', () => { + expect((api as any).buildPath('/foo', {bar: undefined, baz: null})).toBe('/foo'); + }); + + + it('should encode param values as URI components', () => { + expect((api as any).buildPath('/foo', {bar: 'b a&z'})).toBe('/foo?bar=b%20a%26z'); + }); + + }); + + + describe('request()', () => { + let httpsRequestSpy: jasmine.Spy; + let latestRequest: ClientRequest; + + beforeEach(() => { + const originalRequest = https.request; + + httpsRequestSpy = spyOn(https, 'request').and.callFake((...args: any[]) => { + latestRequest = originalRequest.apply(https, args); + + spyOn(latestRequest, 'on').and.callThrough(); + spyOn(latestRequest, 'end'); + + return latestRequest; + }); + }); + + + it('should return a promise', () => { + expect((api as any).request()).toEqual(jasmine.any(Promise)); + }); + + + it('should call \'https.request()\' with the correct options', () => { + (api as any).request('method', 'path'); + + expect(httpsRequestSpy).toHaveBeenCalled(); + expect(httpsRequestSpy.calls.argsFor(0)[0]).toEqual(jasmine.objectContaining({ + headers: jasmine.objectContaining({ + 'User-Agent': `Node/${process.versions.node}`, + }), + host: 'api.github.com', + method: 'method', + path: 'path', + })); + }); + + + it('should call specify an \'Authorization\' header if \'githubToken\' is present', () => { + (api as any).request('method', 'path'); + + expect(httpsRequestSpy).toHaveBeenCalled(); + expect(httpsRequestSpy.calls.argsFor(0)[0].headers).toEqual(jasmine.objectContaining({ + Authorization: 'token 12345', + })); + }); + + + it('should reject on request error', done => { + (api as any).request('method', 'path').catch((err: any) => { + expect(err).toBe('Test'); + done(); + }); + + latestRequest.emit('error', 'Test'); + }); + + + it('should send the request (i.e. call \'end()\')', () => { + (api as any).request('method', 'path'); + expect(latestRequest.end).toHaveBeenCalled(); + }); + + + it('should \'JSON.stringify\' and send the data along with the request', () => { + (api as any).request('method', 'path'); + expect(latestRequest.end).toHaveBeenCalledWith(null); + + (api as any).request('method', 'path', {key: 'value'}); + expect(latestRequest.end).toHaveBeenCalledWith('{"key":"value"}'); + }); + + + describe('onResponse', () => { + let promise: Promise; + let respond: (statusCode: number) => IncomingMessage; + + beforeEach(() => { + promise = (api as any).request('method', 'path'); + + respond = (statusCode: number) => { + const mockResponse = new EventEmitter() as IncomingMessage; + mockResponse.statusCode = statusCode; + + const onResponse = httpsRequestSpy.calls.argsFor(0)[1]; + onResponse(mockResponse); + + return mockResponse; + }; + }); + + + it('should reject on response error', done => { + promise.catch(err => { + expect(err).toBe('Test'); + done(); + }); + + const res = respond(200); + res.emit('error', 'Test'); + }); + + + it('should reject if returned statusCode is <200', done => { + promise.catch(err => { + expect(err).toContain('failed'); + expect(err).toContain('status: 199'); + done(); + }); + + const res = respond(199); + res.emit('end'); + }); + + + it('should reject if returned statusCode is >=400', done => { + promise.catch(err => { + expect(err).toContain('failed'); + expect(err).toContain('status: 400'); + done(); + }); + + const res = respond(400); + res.emit('end'); + }); + + + it('should include the response text in the rejection message', done => { + promise.catch(err => { + expect(err).toContain('Test'); + done(); + }); + + const res = respond(500); + res.emit('data', 'Test'); + res.emit('end'); + }); + + + it('should resolve if returned statusCode is <=200 <400', done => { + promise.then(done); + + const res = respond(200); + res.emit('data', '{}'); + res.emit('end'); + }); + + + it('should resolve with the response text \'JSON.parsed\'', done => { + promise.then(data => { + expect(data).toEqual({foo: 'bar'}); + done(); + }); + + const res = respond(300); + res.emit('data', '{"foo":"bar"}'); + res.emit('end'); + }); + + + it('should collect and concatenate the whole response text', done => { + promise.then(data => { + expect(data).toEqual({foo: 'bar', baz: 'qux'}); + done(); + }); + + const res = respond(300); + res.emit('data', '{"foo":'); + res.emit('data', '"bar","baz"'); + res.emit('data', ':"qux"}'); + res.emit('end'); + }); + + + it('should reject if the response text is malformed JSON', done => { + promise.catch(err => { + expect(err).toEqual(jasmine.any(SyntaxError)); + done(); + }); + + const res = respond(300); + res.emit('data', '}'); + res.emit('end'); + }); + + }); + + }); + +}); diff --git a/aio/aio-builds-setup/dockerbuild/scripts-js/test/common/github-pull-requests.spec.ts b/aio/aio-builds-setup/dockerbuild/scripts-js/test/common/github-pull-requests.spec.ts new file mode 100644 index 0000000000..b7e168eaa0 --- /dev/null +++ b/aio/aio-builds-setup/dockerbuild/scripts-js/test/common/github-pull-requests.spec.ts @@ -0,0 +1,172 @@ +// Imports +import {GithubPullRequests} from '../../lib/common/github-pull-requests'; + +// Tests +describe('GithubPullRequests', () => { + + describe('constructor()', () => { + + it('should log a warning if \'githubToken\' is not defined', () => { + const warningMessage = 'No GitHub access-token specified. Requests will be unauthenticated.'; + const consoleWarnSpy = spyOn(console, 'warn'); + + // tslint:disable-next-line: no-unused-new + new GithubPullRequests('repo/slug'); + + expect(consoleWarnSpy).toHaveBeenCalledWith(warningMessage); + }); + + + it('should throw if \'repoSlag\' is not defined', () => { + expect(() => new GithubPullRequests('', '12345')).toThrowError('Missing required parameter \'repoSlug\'!'); + }); + + }); + + + describe('addComment()', () => { + let prs: GithubPullRequests; + let deferred: {resolve: Function, reject: Function}; + + beforeEach(() => { + prs = new GithubPullRequests('foo/bar', '12345'); + + spyOn(prs, 'post').and.callFake(() => new Promise((resolve, reject) => deferred = {resolve, reject})); + }); + + + it('should return a promise', () => { + expect(prs.addComment(42, 'body')).toEqual(jasmine.any(Promise)); + }); + + + it('should throw if the PR number is invalid', () => { + expect(() => prs.addComment(-1337, 'body')).toThrowError(`Invalid PR number: -1337`); + expect(() => prs.addComment(NaN, 'body')).toThrowError(`Invalid PR number: NaN`); + }); + + + it('should throw if the comment body is invalid or empty', () => { + expect(() => prs.addComment(42, '')).toThrowError(`Invalid or empty comment body: `); + }); + + + it('should call \'post()\' with the correct pathname, params and data', () => { + prs.addComment(42, 'body'); + + expect(prs.post).toHaveBeenCalledWith('/repos/foo/bar/issues/42/comments', null, {body: 'body'}); + }); + + + it('should reject if the request fails', done => { + prs.addComment(42, 'body').catch(err => { + expect(err).toBe('Test'); + done(); + }); + + deferred.reject('Test'); + }); + + + it('should resolve with the returned response', done => { + prs.addComment(42, 'body').then(data => { + expect(data).toEqual('Test'); + done(); + }); + + deferred.resolve('Test'); + }); + + }); + + + describe('fetchAll()', () => { + let prs: GithubPullRequests; + let deferreds: {resolve: Function, reject: Function}[]; + + beforeEach(() => { + prs = new GithubPullRequests('foo/bar', '12345'); + deferreds = []; + + spyOn(process.stdout, 'write'); + spyOn(prs, 'get').and.callFake(() => new Promise((resolve, reject) => deferreds.push({resolve, reject}))); + }); + + + it('should return a promise', () => { + expect(prs.fetchAll()).toEqual(jasmine.any(Promise)); + }); + + + it('should call \'get()\' with the correct pathname and params', () => { + prs.fetchAll('open'); + + expect(prs.get).toHaveBeenCalledWith('/repos/foo/bar/pulls', { + page: 0, + per_page: 100, + state: 'open', + }); + }); + + + it('should default to \'all\' if no state is specified', () => { + prs.fetchAll(); + + expect(prs.get).toHaveBeenCalledWith('/repos/foo/bar/pulls', { + page: 0, + per_page: 100, + state: 'all', + }); + }); + + + it('should reject if the request fails', done => { + prs.fetchAll().catch(err => { + expect(err).toBe('Test'); + done(); + }); + + deferreds[0].reject('Test'); + }); + + + it('should resolve with the returned pull requests', done => { + const pullRequests = [{id: 1}, {id: 2}]; + + prs.fetchAll().then(data => { + expect(data).toEqual(pullRequests); + done(); + }); + + deferreds[0].resolve(pullRequests); + }); + + + it('should iteratively call \'get()\' to fetch all pull requests', done => { + // Create an array or 250 objects. + const allPullRequests = '.'.repeat(250).split('').map((_, i) => ({id: i})); + const prsGetApy = prs.get as jasmine.Spy; + + prs.fetchAll().then(data => { + const paramsForPage = (page: number) => ({page, per_page: 100, state: 'all'}); + + expect(prsGetApy).toHaveBeenCalledTimes(3); + expect(prsGetApy.calls.argsFor(0)).toEqual(['/repos/foo/bar/pulls', paramsForPage(0)]); + expect(prsGetApy.calls.argsFor(1)).toEqual(['/repos/foo/bar/pulls', paramsForPage(1)]); + expect(prsGetApy.calls.argsFor(2)).toEqual(['/repos/foo/bar/pulls', paramsForPage(2)]); + + expect(data).toEqual(allPullRequests); + + done(); + }); + + deferreds[0].resolve(allPullRequests.slice(0, 100)); + setTimeout(() => { + deferreds[1].resolve(allPullRequests.slice(100, 200)); + setTimeout(() => deferreds[2].resolve(allPullRequests.slice(200)), 0); + }, 0); + }); + + }); + +}); diff --git a/aio/aio-builds-setup/dockerbuild/scripts-js/test/common/utils.spec.ts b/aio/aio-builds-setup/dockerbuild/scripts-js/test/common/utils.spec.ts new file mode 100644 index 0000000000..c1177aa1da --- /dev/null +++ b/aio/aio-builds-setup/dockerbuild/scripts-js/test/common/utils.spec.ts @@ -0,0 +1,62 @@ +// Imports +import {getEnvVar} from '../../lib/common/utils'; + +// Tests +describe('utils', () => { + + describe('getEnvVar()', () => { + const emptyVar = '$$test_utils_getEnvVar_empty$$'; + const nonEmptyVar = '$$test_utils_getEnvVar_nonEmpty$$'; + const undefinedVar = '$$test_utils_getEnvVar_undefined$$'; + + beforeEach(() => { + process.env[emptyVar] = ''; + process.env[nonEmptyVar] = 'foo'; + }); + afterEach(() => { + delete process.env[emptyVar]; + delete process.env[nonEmptyVar]; + }); + + + it('should return an environment variable', () => { + expect(getEnvVar(nonEmptyVar)).toBe('foo'); + }); + + + it('should exit with an error if the environment variable is not defined', () => { + const consoleErrorSpy = spyOn(console, 'error'); + const processExitSpy = spyOn(process, 'exit'); + + getEnvVar(undefinedVar); + + expect(consoleErrorSpy).toHaveBeenCalled(); + expect(consoleErrorSpy.calls.argsFor(0)[0]).toContain(undefinedVar); + expect(processExitSpy).toHaveBeenCalledWith(1); + }); + + + it('should exit with an error if the environment variable is empty', () => { + const consoleErrorSpy = spyOn(console, 'error'); + const processExitSpy = spyOn(process, 'exit'); + + getEnvVar(emptyVar); + + expect(consoleErrorSpy).toHaveBeenCalled(); + expect(consoleErrorSpy.calls.argsFor(0)[0]).toContain(emptyVar); + expect(processExitSpy).toHaveBeenCalledWith(1); + }); + + + it('should return an empty string if an undefined variable is optional', () => { + expect(getEnvVar(undefinedVar, true)).toBe(''); + }); + + + it('should return an empty string if an empty variable is optional', () => { + expect(getEnvVar(emptyVar, true)).toBe(''); + }); + + }); + +}); diff --git a/aio/aio-builds-setup/dockerbuild/scripts-js/test/helpers.ts b/aio/aio-builds-setup/dockerbuild/scripts-js/test/helpers.ts new file mode 100644 index 0000000000..044af2b207 --- /dev/null +++ b/aio/aio-builds-setup/dockerbuild/scripts-js/test/helpers.ts @@ -0,0 +1,6 @@ +declare namespace jasmine { + export interface DoneFn extends Function { + (): void; + fail: (message: Error | string) => void; + } +} diff --git a/aio/aio-builds-setup/dockerbuild/scripts-js/test/index.ts b/aio/aio-builds-setup/dockerbuild/scripts-js/test/index.ts new file mode 100644 index 0000000000..487536920c --- /dev/null +++ b/aio/aio-builds-setup/dockerbuild/scripts-js/test/index.ts @@ -0,0 +1,7 @@ +// Imports +import {runTests} from '../lib/common/run-tests'; + +// Run +const specFiles = [`${__dirname}/**/*.spec.js`]; +const helpers = [`${__dirname}/helpers.js`]; +runTests(specFiles, helpers); diff --git a/aio/aio-builds-setup/dockerbuild/scripts-js/test/upload-server/build-creator.spec.ts b/aio/aio-builds-setup/dockerbuild/scripts-js/test/upload-server/build-creator.spec.ts new file mode 100644 index 0000000000..92435cdd97 --- /dev/null +++ b/aio/aio-builds-setup/dockerbuild/scripts-js/test/upload-server/build-creator.spec.ts @@ -0,0 +1,330 @@ +// Imports +import * as cp from 'child_process'; +import {EventEmitter} from 'events'; +import * as fs from 'fs'; +import * as shell from 'shelljs'; +import {BuildCreator} from '../../lib/upload-server/build-creator'; +import {CreatedBuildEvent} from '../../lib/upload-server/build-events'; +import {UploadError} from '../../lib/upload-server/upload-error'; + +// Tests +describe('BuildCreator', () => { + const pr = '9'; + const sha = '9'.repeat(40); + const archive = 'snapshot.tar.gz'; + const buildsDir = 'build/dir'; + const prDir = `${buildsDir}/${pr}`; + const shaDir = `${prDir}/${sha}`; + let bc: BuildCreator; + + // Helpers + const expectToBeUploadError = (actual: UploadError, expStatus?: number, expMessage?: string) => { + expect(actual).toEqual(jasmine.any(UploadError)); + if (expStatus != null) { + expect(actual.status).toBe(expStatus); + } + if (expMessage != null) { + expect(actual.message).toBe(expMessage); + } + }; + + beforeEach(() => bc = new BuildCreator(buildsDir)); + + + describe('constructor()', () => { + + it('should throw if \'buildsDir\' is missing or empty', () => { + expect(() => new BuildCreator('')).toThrowError('Missing or empty required parameter \'buildsDir\'!'); + }); + + + it('should extend EventEmitter', () => { + expect(bc).toEqual(jasmine.any(BuildCreator)); + expect(bc).toEqual(jasmine.any(EventEmitter)); + + expect(Object.getPrototypeOf(bc)).toBe(BuildCreator.prototype); + }); + + }); + + + describe('create()', () => { + let bcEmitSpy: jasmine.Spy; + let bcExistsSpy: jasmine.Spy; + let bcExtractArchiveSpy: jasmine.Spy; + let shellMkdirSpy: jasmine.Spy; + let shellRmSpy: jasmine.Spy; + + beforeEach(() => { + bcEmitSpy = spyOn(bc, 'emit'); + bcExistsSpy = spyOn(bc as any, 'exists'); + bcExtractArchiveSpy = spyOn(bc as any, 'extractArchive'); + shellMkdirSpy = spyOn(shell, 'mkdir'); + shellRmSpy = spyOn(shell, 'rm'); + }); + + + it('should return a promise', done => { + const promise = bc.create(pr, sha, archive); + promise.then(done); // Do not complete the test (and release the spies) synchronously + // to avoid running the actual `extractArchive()`. + + expect(promise).toEqual(jasmine.any(Promise)); + }); + + + it('should throw if the build does already exist', done => { + bcExistsSpy.and.returnValue(true); + bc.create(pr, sha, archive).catch(err => { + expectToBeUploadError(err, 403, `Request to overwrite existing directory: ${shaDir}`); + done(); + }); + }); + + + it('should create the build directory (and any parent directories)', done => { + bc.create(pr, sha, archive). + then(() => expect(shellMkdirSpy).toHaveBeenCalledWith('-p', shaDir)). + then(done); + }); + + + it('should extract the archive contents into the build directory', done => { + bc.create(pr, sha, archive). + then(() => expect(bcExtractArchiveSpy).toHaveBeenCalledWith(archive, shaDir)). + then(done); + }); + + + it('should emit a CreatedBuildEvent on success', done => { + let emitted = false; + + bcEmitSpy.and.callFake((type: string, evt: CreatedBuildEvent) => { + expect(type).toBe(CreatedBuildEvent.type); + expect(evt).toEqual(jasmine.any(CreatedBuildEvent)); + expect(evt.pr).toBe(+pr); + expect(evt.sha).toBe(sha); + + emitted = true; + }); + + bc.create(pr, sha, archive). + then(() => expect(emitted).toBe(true)). + then(done); + }); + + + describe('on error', () => { + + it('should abort and skip further operations if it fails to create the directories', done => { + shellMkdirSpy.and.throwError(''); + bc.create(pr, sha, archive).catch(() => { + expect(shellMkdirSpy).toHaveBeenCalled(); + expect(bcExtractArchiveSpy).not.toHaveBeenCalled(); + expect(bcEmitSpy).not.toHaveBeenCalled(); + done(); + }); + }); + + + it('should abort and skip further operations if it fails to extract the archive', done => { + bcExtractArchiveSpy.and.throwError(''); + bc.create(pr, sha, archive).catch(() => { + expect(shellMkdirSpy).toHaveBeenCalled(); + expect(bcExtractArchiveSpy).toHaveBeenCalled(); + expect(bcEmitSpy).not.toHaveBeenCalled(); + done(); + }); + }); + + + it('should delete the PR directory (for new PR)', done => { + bcExtractArchiveSpy.and.throwError(''); + bc.create(pr, sha, archive).catch(() => { + expect(shellRmSpy).toHaveBeenCalledWith('-rf', prDir); + done(); + }); + }); + + + it('should delete the SHA directory (for existing PR)', done => { + bcExistsSpy.and.callFake((path: string) => path !== shaDir); + bcExtractArchiveSpy.and.throwError(''); + + bc.create(pr, sha, archive).catch(() => { + expect(shellRmSpy).toHaveBeenCalledWith('-rf', shaDir); + done(); + }); + }); + + + it('should reject with an UploadError', done => { + shellMkdirSpy.and.callFake(() => {throw 'Test'; }); + bc.create(pr, sha, archive).catch(err => { + expectToBeUploadError(err, 500, `Error while uploading to directory: ${shaDir}\nTest`); + done(); + }); + }); + + + it('should pass UploadError instances unmodified', done => { + shellMkdirSpy.and.callFake(() => { throw new UploadError(543, 'Test'); }); + bc.create(pr, sha, archive).catch(err => { + expectToBeUploadError(err, 543, 'Test'); + done(); + }); + }); + + }); + + }); + + + // Protected methods + + describe('exists()', () => { + let fsAccessSpy: jasmine.Spy; + let fsAccessCbs: Function[]; + + beforeEach(() => { + fsAccessCbs = []; + fsAccessSpy = spyOn(fs, 'access').and.callFake((_: string, cb: Function) => fsAccessCbs.push(cb)); + }); + + + it('should return a promise', () => { + expect((bc as any).exists('foo')).toEqual(jasmine.any(Promise)); + }); + + + it('should call \'fs.access()\' with the specified argument', () => { + (bc as any).exists('foo'); + expect(fs.access).toHaveBeenCalledWith('foo', jasmine.any(Function)); + }); + + + it('should resolve with \'true\' if \'fs.access()\' succeeds', done => { + Promise. + all([(bc as any).exists('foo'), (bc as any).exists('bar')]). + then(results => expect(results).toEqual([true, true])). + then(done); + + fsAccessCbs[0](); + fsAccessCbs[1](null); + }); + + + it('should resolve with \'false\' if \'fs.access()\' errors', done => { + Promise. + all([(bc as any).exists('foo'), (bc as any).exists('bar')]). + then(results => expect(results).toEqual([false, false])). + then(done); + + fsAccessCbs[0]('Error'); + fsAccessCbs[1](new Error()); + }); + + }); + + + describe('extractArchive()', () => { + let consoleWarnSpy: jasmine.Spy; + let shellChmodSpy: jasmine.Spy; + let shellRmSpy: jasmine.Spy; + let cpExecSpy: jasmine.Spy; + let cpExecCbs: Function[]; + + beforeEach(() => { + cpExecCbs = []; + + consoleWarnSpy = spyOn(console, 'warn'); + shellChmodSpy = spyOn(shell, 'chmod'); + shellRmSpy = spyOn(shell, 'rm'); + cpExecSpy = spyOn(cp, 'exec').and.callFake((_: string, cb: Function) => cpExecCbs.push(cb)); + }); + + + it('should return a promise', () => { + expect((bc as any).extractArchive('foo', 'bar')).toEqual(jasmine.any(Promise)); + }); + + + it('should "gunzip" and "untar" the input file into the output directory', () => { + const cmd = 'tar --extract --gzip --directory "output/dir" --file "input/file"'; + + (bc as any).extractArchive('input/file', 'output/dir'); + expect(cpExecSpy).toHaveBeenCalledWith(cmd, jasmine.any(Function)); + }); + + + it('should log (as a warning) any stderr output if extracting succeeded', done => { + (bc as any).extractArchive('foo', 'bar'). + then(() => expect(consoleWarnSpy).toHaveBeenCalledWith('This is stderr')). + then(done); + + cpExecCbs[0](null, 'This is stdout', 'This is stderr'); + }); + + + it('should make the build directory non-writable', done => { + (bc as any).extractArchive('foo', 'bar'). + then(() => expect(shellChmodSpy).toHaveBeenCalledWith('-R', 'a-w', 'bar')). + then(done); + + cpExecCbs[0](); + }); + + + it('should delete the uploaded file on success', done => { + (bc as any).extractArchive('input/file', 'output/dir'). + then(() => expect(shellRmSpy).toHaveBeenCalledWith('-f', 'input/file')). + then(done); + + cpExecCbs[0](); + }); + + + describe('on error', () => { + + it('should abort and skip further operations if it fails to extract the archive', done => { + (bc as any).extractArchive('foo', 'bar').catch((err: any) => { + expect(shellChmodSpy).not.toHaveBeenCalled(); + expect(shellRmSpy).not.toHaveBeenCalled(); + expect(err).toBe('Test'); + done(); + }); + + cpExecCbs[0]('Test'); + }); + + + it('should abort and skip further operations if it fails to make non-writable', done => { + (bc as any).extractArchive('foo', 'bar').catch((err: any) => { + expect(shellChmodSpy).toHaveBeenCalled(); + expect(shellRmSpy).not.toHaveBeenCalled(); + expect(err).toBe('Test'); + done(); + }); + + shellChmodSpy.and.callFake(() => { throw 'Test'; }); + cpExecCbs[0](); + }); + + + it('should abort and reject if it fails to remove the uploaded file', done => { + (bc as any).extractArchive('foo', 'bar').catch((err: any) => { + expect(shellChmodSpy).toHaveBeenCalled(); + expect(shellRmSpy).toHaveBeenCalled(); + expect(err).toBe('Test'); + done(); + }); + + shellRmSpy.and.callFake(() => { throw 'Test'; }); + cpExecCbs[0](); + }); + + }); + + }); + +}); diff --git a/aio/aio-builds-setup/dockerbuild/scripts-js/test/upload-server/build-events.spec.ts b/aio/aio-builds-setup/dockerbuild/scripts-js/test/upload-server/build-events.spec.ts new file mode 100644 index 0000000000..868e6ce352 --- /dev/null +++ b/aio/aio-builds-setup/dockerbuild/scripts-js/test/upload-server/build-events.spec.ts @@ -0,0 +1,61 @@ +// Imports +import {BuildEvent, CreatedBuildEvent} from '../../lib/upload-server/build-events'; + +// Tests +describe('BuildEvent', () => { + let evt: BuildEvent; + + beforeEach(() => evt = new BuildEvent('foo', 42, 'bar')); + + + it('should have a \'type\' property', () => { + expect(evt.type).toBe('foo'); + }); + + + it('should have a \'pr\' property', () => { + expect(evt.pr).toBe(42); + }); + + + it('should have a \'sha\' property', () => { + expect(evt.sha).toBe('bar'); + }); + +}); + + +describe('CreatedBuildEvent', () => { + let evt: CreatedBuildEvent; + + beforeEach(() => evt = new CreatedBuildEvent(42, 'bar')); + + + it('should have a static \'type\' property', () => { + expect(CreatedBuildEvent.type).toBe('build.created'); + }); + + + it('should extend BuildEvent', () => { + expect(evt).toEqual(jasmine.any(CreatedBuildEvent)); + expect(evt).toEqual(jasmine.any(BuildEvent)); + + expect(Object.getPrototypeOf(evt)).toBe(CreatedBuildEvent.prototype); + }); + + + it('should automatically set the \'type\'', () => { + expect(evt.type).toBe(CreatedBuildEvent.type); + }); + + + it('should have a \'pr\' property', () => { + expect(evt.pr).toBe(42); + }); + + + it('should have a \'sha\' property', () => { + expect(evt.sha).toBe('bar'); + }); + +}); diff --git a/aio/aio-builds-setup/dockerbuild/scripts-js/test/upload-server/upload-error.spec.ts b/aio/aio-builds-setup/dockerbuild/scripts-js/test/upload-server/upload-error.spec.ts new file mode 100644 index 0000000000..9a20925932 --- /dev/null +++ b/aio/aio-builds-setup/dockerbuild/scripts-js/test/upload-server/upload-error.spec.ts @@ -0,0 +1,39 @@ +// Imports +import {UploadError} from '../../lib/upload-server/upload-error'; + +// Tests +describe('UploadError', () => { + let err: UploadError; + + beforeEach(() => err = new UploadError(999, 'message')); + + + it('should extend Error', () => { + expect(err).toEqual(jasmine.any(UploadError)); + expect(err).toEqual(jasmine.any(Error)); + + expect(Object.getPrototypeOf(err)).toBe(UploadError.prototype); + }); + + + it('should have a \'status\' property', () => { + expect(err.status).toBe(999); + }); + + + it('should have a \'message\' property', () => { + expect(err.message).toBe('message'); + }); + + + it('should have a 500 \'status\' by default', () => { + expect(new UploadError().status).toBe(500); + }); + + + it('should have an empty \'message\' by default', () => { + expect(new UploadError().message).toBe(''); + expect(new UploadError(999).message).toBe(''); + }); + +}); diff --git a/aio/aio-builds-setup/dockerbuild/scripts-js/test/upload-server/upload-server-factory.spec.ts b/aio/aio-builds-setup/dockerbuild/scripts-js/test/upload-server/upload-server-factory.spec.ts new file mode 100644 index 0000000000..26d74ff1e6 --- /dev/null +++ b/aio/aio-builds-setup/dockerbuild/scripts-js/test/upload-server/upload-server-factory.spec.ts @@ -0,0 +1,262 @@ +// Imports +import * as express from 'express'; +import * as http from 'http'; +import * as supertest from 'supertest'; +import {BuildCreator} from '../../lib/upload-server/build-creator'; +import {CreatedBuildEvent} from '../../lib/upload-server/build-events'; +import {uploadServerFactory as usf} from '../../lib/upload-server/upload-server-factory'; + +// Tests +describe('uploadServerFactory', () => { + + describe('create()', () => { + let usfCreateMiddlewareSpy: jasmine.Spy; + + beforeEach(() => { + usfCreateMiddlewareSpy = spyOn(usf as any, 'createMiddleware').and.callThrough(); + }); + + + it('should throw if \'buildsDir\' is empty', () => { + expect(() => usf.create('')).toThrowError('Missing or empty required parameter \'buildsDir\'!'); + }); + + + it('should return an http.Server', () => { + const httpCreateServerSpy = spyOn(http, 'createServer').and.callThrough(); + const server = usf.create('builds/dir'); + + expect(server).toBe(httpCreateServerSpy.calls.mostRecent().returnValue); + }); + + + it('should create and use an appropriate middleware', () => { + const httpCreateServerSpy = spyOn(http, 'createServer').and.callThrough(); + + usf.create('builds/dir'); + const middleware: express.Express = usfCreateMiddlewareSpy.calls.mostRecent().returnValue; + + expect(usfCreateMiddlewareSpy).toHaveBeenCalledWith(jasmine.any(BuildCreator)); + expect(httpCreateServerSpy).toHaveBeenCalledWith(middleware); + }); + + + it('should pass \'buildsDir\' to the created BuildCreator', () => { + usf.create('builds/dir'); + const buildCreator: BuildCreator = usfCreateMiddlewareSpy.calls.argsFor(0)[0]; + + expect((buildCreator as any).buildsDir).toBe('builds/dir'); + }); + + + it('should pass CreatedBuildEvents emitted on BuildCreator through to the server', done => { + const server = usf.create('builds/dir'); + const buildCreator: BuildCreator = usfCreateMiddlewareSpy.calls.argsFor(0)[0]; + const evt = new CreatedBuildEvent(42, 'foo'); + + server.on(CreatedBuildEvent.type, (data: CreatedBuildEvent) => { + expect(data).toBe(evt); + done(); + }); + + buildCreator.emit(CreatedBuildEvent.type, evt); + }); + + + it('should log the server address info on \'listening\'', () => { + const consoleInfoSpy = spyOn(console, 'info'); + const server = usf.create('builds/dir'); + server.address = () => ({address: 'foo', family: '', port: 1337}); + + expect(consoleInfoSpy).not.toHaveBeenCalled(); + + server.emit('listening'); + expect(consoleInfoSpy).toHaveBeenCalledWith('Up and running (and listening on foo:1337)...'); + }); + + }); + + + // Protected methods + + describe('createMiddleware()', () => { + let buildCreator: BuildCreator; + let agent: supertest.SuperTest; + + // Helpers + const promisifyRequest = (req: supertest.Request) => + new Promise((resolve, reject) => req.end(err => err ? reject(err) : resolve())); + const verifyRequests = (reqs: supertest.Request[], done: jasmine.DoneFn) => + Promise.all(reqs.map(promisifyRequest)).then(done, done.fail); + + beforeEach(() => { + buildCreator = new BuildCreator('builds/dir'); + agent = supertest.agent((usf as any).createMiddleware(buildCreator)); + + spyOn(console, 'error'); + }); + + + describe('GET /create-build//', () => { + const pr = '9'; + const sha = '9'.repeat(40); + let buildCreatorCreateSpy: jasmine.Spy; + let deferred: {resolve: Function, reject: Function}; + + beforeEach(() => { + const promise = new Promise((resolve, reject) => deferred = {resolve, reject}); + promise.catch(() => null); // Avoid "unhandled rejection" warnings. + + buildCreatorCreateSpy = spyOn(buildCreator, 'create').and.returnValue(promise); + }); + + + it('should respond with 405 for non-GET requests', done => { + verifyRequests([ + agent.put(`/create-build/${pr}/${sha}`).expect(405), + agent.post(`/create-build/${pr}/${sha}`).expect(405), + agent.patch(`/create-build/${pr}/${sha}`).expect(405), + agent.delete(`/create-build/${pr}/${sha}`).expect(405), + ], done); + }); + + + it('should respond with 400 for requests without an \'X-FILE\' header', done => { + const url = `/create-build/${pr}/${sha}`; + const responseBody = `Missing or empty 'X-FILE' header in request: GET ${url}`; + + verifyRequests([ + agent.get(url).expect(400, responseBody), + agent.get(url).field('X-FILE', '').expect(400, responseBody), + ], done); + }); + + + it('should respond with 404 for unknown paths', done => { + verifyRequests([ + agent.get(`/foo/create-build/${pr}/${sha}`).expect(404), + agent.get(`/foo-create-build/${pr}/${sha}`).expect(404), + agent.get(`/fooncreate-build/${pr}/${sha}`).expect(404), + agent.get(`/create-build/foo/${pr}/${sha}`).expect(404), + agent.get(`/create-build-foo/${pr}/${sha}`).expect(404), + agent.get(`/create-buildnfoo/${pr}/${sha}`).expect(404), + agent.get(`/create-build/pr${pr}/${sha}`).expect(404), + agent.get(`/create-build/${pr}/${sha}42`).expect(404), + ], done); + }); + + + it('should propagate errors from BuildCreator', done => { + const req = agent. + get(`/create-build/${pr}/${sha}`). + set('X-FILE', 'foo'). + expect(500, 'Test'); + + verifyRequests([req], done); + deferred.reject('Test'); + }); + + + it('should respond with 201 on successful upload', done => { + const req = agent. + get(`/create-build/${pr}/${sha}`). + set('X-FILE', 'foo'). + expect(201, http.STATUS_CODES[201]); + + verifyRequests([req], done); + deferred.resolve(); + }); + + + it('should call \'BuildCreator#create()\' with appropriate arguments', done => { + promisifyRequest(agent.get(`/create-build/${pr}/${sha}`).set('X-FILE', 'foo').expect(201)). + then(() => expect(buildCreatorCreateSpy).toHaveBeenCalledWith(pr, sha, 'foo')). + then(done, done.fail); + + deferred.resolve(); + }); + + + it('should reject PRs with leading zeros', done => { + verifyRequests([agent.get(`/create-build/0${pr}/${sha}`).expect(404)], done); + }); + + + it('should accept SHAs with leading zeros (but not ignore them)', done => { + const sha40 = '0'.repeat(40); + const sha41 = `0${sha40}`; + + Promise.all([ + promisifyRequest(agent.get(`/create-build/${pr}/${sha41}`).expect(404)), + promisifyRequest(agent.get(`/create-build/${pr}/${sha40}`).set('X-FILE', 'foo')). + then(() => expect(buildCreatorCreateSpy).toHaveBeenCalledWith(pr, sha40, 'foo')), + ]).then(done, done.fail); + + deferred.resolve(); + }); + + }); + + + describe('GET /health-check', () => { + + it('should respond with 200', done => { + verifyRequests([ + agent.get('/health-check').expect(200), + agent.get('/health-check/').expect(200), + ], done); + }); + + + it('should respond with 405 for non-GET requests', done => { + verifyRequests([ + agent.put('/health-check').expect(405), + agent.post('/health-check').expect(405), + agent.patch('/health-check').expect(405), + agent.delete('/health-check').expect(405), + ], done); + }); + + + it('should respond with 404 if the path does not match exactly', done => { + verifyRequests([ + agent.get('/health-check/foo').expect(404), + agent.get('/health-check-foo').expect(404), + agent.get('/health-checknfoo').expect(404), + agent.get('/foo/health-check').expect(404), + agent.get('/foo-health-check').expect(404), + agent.get('/foonhealth-check').expect(404), + ], done); + }); + + }); + + + describe('GET *', () => { + + it('should respond with 404', done => { + const responseBody = 'Unknown resource in request: GET /some/url'; + verifyRequests([agent.get('/some/url').expect(404, responseBody)], done); + }); + + }); + + + describe('ALL *', () => { + + it('should respond with 405', done => { + const responseFor = (method: string) => `Unsupported method in request: ${method.toUpperCase()} /some/url`; + + verifyRequests([ + agent.put('/some/url').expect(405, responseFor('put')), + agent.post('/some/url').expect(405, responseFor('post')), + agent.patch('/some/url').expect(405, responseFor('patch')), + agent.delete('/some/url').expect(405, responseFor('delete')), + ], done); + }); + + }); + + }); + +}); diff --git a/aio/aio-builds-setup/dockerbuild/scripts-js/tsconfig.json b/aio/aio-builds-setup/dockerbuild/scripts-js/tsconfig.json new file mode 100644 index 0000000000..d2a4454806 --- /dev/null +++ b/aio/aio-builds-setup/dockerbuild/scripts-js/tsconfig.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + "alwaysStrict": true, + "forceConsistentCasingInFileNames": true, + "inlineSourceMap": true, + "lib": [ + "es2016" + ], + "noImplicitAny": true, + "noImplicitReturns": true, + "noImplicitThis": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "outDir": "dist", + "pretty": true, + "rootDir": ".", + "skipLibCheck": true, + "strictNullChecks": true, + "target": "es5", + "typeRoots": [ + "node_modules/@types" + ] + }, + "include": [ + "lib/**/*", + "test/**/*" + ] +} diff --git a/aio/aio-builds-setup/dockerbuild/scripts-js/tslint.json b/aio/aio-builds-setup/dockerbuild/scripts-js/tslint.json new file mode 100644 index 0000000000..d773f53668 --- /dev/null +++ b/aio/aio-builds-setup/dockerbuild/scripts-js/tslint.json @@ -0,0 +1,15 @@ +{ + "extends": "tslint:recommended", + "rules": { + "array-type": [true, "array"], + "arrow-parens": [true, "ban-single-arg-parens"], + "interface-name": [true, "never-prefix"], + "max-classes-per-file": [true, 4], + "no-consecutive-blank-lines": [true, 2], + "no-console": false, + "no-namespace": [true, "allow-declarations"], + "no-string-literal": false, + "quotemark": [true, "single"], + "variable-name": [true, "ban-keywords", "check-format", "allow-leading-underscore"] + } +} diff --git a/aio/aio-builds-setup/dockerbuild/scripts-js/yarn.lock b/aio/aio-builds-setup/dockerbuild/scripts-js/yarn.lock new file mode 100644 index 0000000000..1e3900a1b9 --- /dev/null +++ b/aio/aio-builds-setup/dockerbuild/scripts-js/yarn.lock @@ -0,0 +1,2461 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@types/express-serve-static-core@*": + version "4.0.40" + resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.0.40.tgz#168e82978bffc81ee7737bc60728d64733a4f37b" + dependencies: + "@types/node" "*" + +"@types/express@^4.0.35": + version "4.0.35" + resolved "https://registry.yarnpkg.com/@types/express/-/express-4.0.35.tgz#6267c7b60a51fac473467b3c4a02cd1e441805fe" + dependencies: + "@types/express-serve-static-core" "*" + "@types/serve-static" "*" + +"@types/jasmine@^2.5.43": + version "2.5.43" + resolved "https://registry.yarnpkg.com/@types/jasmine/-/jasmine-2.5.43.tgz#6328a8c26082f2fd84f043c802c9ed7fa110b2dd" + dependencies: + typescript ">=2.1.4" + +"@types/mime@*": + version "0.0.29" + resolved "https://registry.yarnpkg.com/@types/mime/-/mime-0.0.29.tgz#fbcfd330573b912ef59eeee14602bface630754b" + +"@types/node@*", "@types/node@^7.0.5": + version "7.0.5" + resolved "https://registry.yarnpkg.com/@types/node/-/node-7.0.5.tgz#96a0f0a618b7b606f1ec547403c00650210bfbb7" + +"@types/serve-static@*": + version "1.7.31" + resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.7.31.tgz#15456de8d98d6b4cff31be6c6af7492ae63f521a" + dependencies: + "@types/express-serve-static-core" "*" + "@types/mime" "*" + +"@types/shelljs@^0.7.0": + version "0.7.0" + resolved "https://registry.yarnpkg.com/@types/shelljs/-/shelljs-0.7.0.tgz#229c157c6bc1e67d6b990e6c5e18dbd2ff58cff0" + dependencies: + "@types/node" "*" + +"@types/superagent@*": + version "2.0.36" + resolved "https://registry.yarnpkg.com/@types/superagent/-/superagent-2.0.36.tgz#e8eec10771d9dbc0f7ec47b8f9993476e4a501c1" + dependencies: + "@types/node" "*" + +"@types/supertest@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@types/supertest/-/supertest-2.0.0.tgz#4687deaff4950b63e4611536d6fd44665ee11bb7" + dependencies: + "@types/superagent" "*" + +abbrev@1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.0.tgz#d0554c2256636e2f56e7c2e5ad183f859428d81f" + +accepts@~1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.3.tgz#c3ca7434938648c3e0d9c1e328dd68b622c284ca" + dependencies: + mime-types "~2.1.11" + negotiator "0.6.1" + +acorn-jsx@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-3.0.1.tgz#afdf9488fb1ecefc8348f6fb22f464e32a58b36b" + dependencies: + acorn "^3.0.4" + +acorn@4.0.4: + version "4.0.4" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-4.0.4.tgz#17a8d6a7a6c4ef538b814ec9abac2779293bf30a" + +acorn@^3.0.4: + version "3.3.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-3.3.0.tgz#45e37fb39e8da3f25baee3ff5369e2bb5f22017a" + +ajv-keywords@^1.0.0: + version "1.5.1" + resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-1.5.1.tgz#314dd0a4b3368fad3dfcdc54ede6171b886daf3c" + +ajv@^4.7.0: + version "4.11.3" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-4.11.3.tgz#ce30bdb90d1254f762c75af915fb3a63e7183d22" + dependencies: + co "^4.6.0" + json-stable-stringify "^1.0.1" + +ansi-align@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/ansi-align/-/ansi-align-1.1.0.tgz#2f0c1658829739add5ebb15e6b0c6e3423f016ba" + dependencies: + string-width "^1.0.1" + +ansi-escapes@^1.1.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-1.4.0.tgz#d3a8a83b319aa67793662b13e761c7911422306e" + +ansi-regex@^0.2.0, ansi-regex@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-0.2.1.tgz#0d8e946967a3d8143f93e24e298525fc1b2235f9" + +ansi-regex@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" + +ansi-styles@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-1.1.0.tgz#eaecbf66cd706882760b2f4691582b8f55d7a7de" + +ansi-styles@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe" + +anymatch@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-1.3.0.tgz#a3e52fa39168c825ff57b0248126ce5a8ff95507" + dependencies: + arrify "^1.0.0" + micromatch "^2.1.5" + +aproba@^1.0.3: + version "1.1.1" + resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.1.1.tgz#95d3600f07710aa0e9298c726ad5ecf2eacbabab" + +are-we-there-yet@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-1.1.2.tgz#80e470e95a084794fe1899262c5667c6e88de1b3" + dependencies: + delegates "^1.0.0" + readable-stream "^2.0.0 || ^1.1.13" + +argparse@^1.0.7: + version "1.0.9" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.9.tgz#73d83bc263f86e97f8cc4f6bae1b0e90a7d22c86" + dependencies: + sprintf-js "~1.0.2" + +arr-diff@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-2.0.0.tgz#8f3b827f955a8bd669697e4a4256ac3ceae356cf" + dependencies: + arr-flatten "^1.0.1" + +arr-flatten@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/arr-flatten/-/arr-flatten-1.0.1.tgz#e5ffe54d45e19f32f216e91eb99c8ce892bb604b" + +array-flatten@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" + +array-union@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/array-union/-/array-union-1.0.2.tgz#9a34410e4f4e3da23dea375be5be70f24778ec39" + dependencies: + array-uniq "^1.0.1" + +array-uniq@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/array-uniq/-/array-uniq-1.0.3.tgz#af6ac877a25cc7f74e058894753858dfdb24fdb6" + +array-unique@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.2.1.tgz#a1d97ccafcbc2625cc70fadceb36a50c58b01a53" + +arrify@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d" + +asn1@~0.2.3: + version "0.2.3" + resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.3.tgz#dac8787713c9966849fc8180777ebe9c1ddf3b86" + +assert-plus@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-0.2.0.tgz#d74e1b87e7affc0db8aadb7021f3fe48101ab234" + +assert-plus@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" + +async-each@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.1.tgz#19d386a1d9edc6e7c1c85d388aedbcc56d33602d" + +asynckit@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" + +aws-sign2@~0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.6.0.tgz#14342dd38dbcc94d0e5b87d763cd63612c0e794f" + +aws4@^1.2.1: + version "1.6.0" + resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.6.0.tgz#83ef5ca860b2b32e4a0deedee8c771b9db57471e" + +babel-code-frame@^6.16.0, babel-code-frame@^6.20.0: + version "6.22.0" + resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.22.0.tgz#027620bee567a88c32561574e7fd0801d33118e4" + dependencies: + chalk "^1.1.0" + esutils "^2.0.2" + js-tokens "^3.0.0" + +balanced-match@^0.4.1: + version "0.4.2" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-0.4.2.tgz#cb3f3e3c732dc0f01ee70b403f302e61d7709838" + +bcrypt-pbkdf@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz#63bc5dcb61331b92bc05fd528953c33462a06f8d" + dependencies: + tweetnacl "^0.14.3" + +binary-extensions@^1.0.0: + version "1.8.0" + resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.8.0.tgz#48ec8d16df4377eae5fa5884682480af4d95c774" + +block-stream@*: + version "0.0.9" + resolved "https://registry.yarnpkg.com/block-stream/-/block-stream-0.0.9.tgz#13ebfe778a03205cfe03751481ebb4b3300c126a" + dependencies: + inherits "~2.0.0" + +boom@2.x.x: + version "2.10.1" + resolved "https://registry.yarnpkg.com/boom/-/boom-2.10.1.tgz#39c8918ceff5799f83f9492a848f625add0c766f" + dependencies: + hoek "2.x.x" + +boxen@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/boxen/-/boxen-0.6.0.tgz#8364d4248ac34ff0ef1b2f2bf49a6c60ce0d81b6" + dependencies: + ansi-align "^1.1.0" + camelcase "^2.1.0" + chalk "^1.1.1" + cli-boxes "^1.0.0" + filled-array "^1.0.0" + object-assign "^4.0.1" + repeating "^2.0.0" + string-width "^1.0.1" + widest-line "^1.0.0" + +brace-expansion@^1.0.0: + version "1.1.6" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.6.tgz#7197d7eaa9b87e648390ea61fc66c84427420df9" + dependencies: + balanced-match "^0.4.1" + concat-map "0.0.1" + +braces@^1.8.2: + version "1.8.5" + resolved "https://registry.yarnpkg.com/braces/-/braces-1.8.5.tgz#ba77962e12dff969d6b76711e914b737857bf6a7" + dependencies: + expand-range "^1.8.1" + preserve "^0.2.0" + repeat-element "^1.1.2" + +buffer-shims@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/buffer-shims/-/buffer-shims-1.0.0.tgz#9978ce317388c649ad8793028c3477ef044a8b51" + +caller-path@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/caller-path/-/caller-path-0.1.0.tgz#94085ef63581ecd3daa92444a8fe94e82577751f" + dependencies: + callsites "^0.2.0" + +callsites@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/callsites/-/callsites-0.2.0.tgz#afab96262910a7f33c19a5775825c69f34e350ca" + +camelcase@^2.1.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-2.1.1.tgz#7c1d16d679a1bbe59ca02cacecfb011e201f5a1f" + +capture-stack-trace@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/capture-stack-trace/-/capture-stack-trace-1.0.0.tgz#4a6fa07399c26bba47f0b2496b4d0fb408c5550d" + +caseless@~0.11.0: + version "0.11.0" + resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.11.0.tgz#715b96ea9841593cc33067923f5ec60ebda4f7d7" + +chalk@0.5.1: + version "0.5.1" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-0.5.1.tgz#663b3a648b68b55d04690d49167aa837858f2174" + dependencies: + ansi-styles "^1.1.0" + escape-string-regexp "^1.0.0" + has-ansi "^0.1.0" + strip-ansi "^0.3.0" + supports-color "^0.2.0" + +chalk@^1.0.0, chalk@^1.1.0, chalk@^1.1.1, chalk@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" + dependencies: + ansi-styles "^2.2.1" + escape-string-regexp "^1.0.2" + has-ansi "^2.0.0" + strip-ansi "^3.0.0" + supports-color "^2.0.0" + +chokidar@^1.4.3: + version "1.6.1" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-1.6.1.tgz#2f4447ab5e96e50fb3d789fd90d4c72e0e4c70c2" + dependencies: + anymatch "^1.3.0" + async-each "^1.0.0" + glob-parent "^2.0.0" + inherits "^2.0.1" + is-binary-path "^1.0.0" + is-glob "^2.0.0" + path-is-absolute "^1.0.0" + readdirp "^2.0.0" + optionalDependencies: + fsevents "^1.0.0" + +circular-json@^0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/circular-json/-/circular-json-0.3.1.tgz#be8b36aefccde8b3ca7aa2d6afc07a37242c0d2d" + +cli-boxes@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/cli-boxes/-/cli-boxes-1.0.0.tgz#4fa917c3e59c94a004cd61f8ee509da651687143" + +cli-cursor@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-1.0.2.tgz#64da3f7d56a54412e59794bd62dc35295e8f2987" + dependencies: + restore-cursor "^1.0.1" + +cli-width@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.1.0.tgz#b234ca209b29ef66fc518d9b98d5847b00edf00a" + +co@^4.6.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" + +code-point-at@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" + +colors@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/colors/-/colors-1.1.2.tgz#168a4701756b6a7f51a12ce0c97bfa28c084ed63" + +combined-stream@^1.0.5, combined-stream@~1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.5.tgz#938370a57b4a51dea2c77c15d5c5fdf895164009" + dependencies: + delayed-stream "~1.0.0" + +commander@2.6.0: + version "2.6.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.6.0.tgz#9df7e52fb2a0cb0fb89058ee80c3104225f37e1d" + +commander@^2.9.0: + version "2.9.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.9.0.tgz#9c99094176e12240cb22d6c5146098400fe0f7d4" + dependencies: + graceful-readlink ">= 1.0.0" + +component-emitter@^1.2.0: + version "1.2.1" + resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.2.1.tgz#137918d6d78283f7df7a6b7c5a63e140e69425e6" + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + +concat-stream@^1.4.6: + version "1.6.0" + resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.0.tgz#0aac662fd52be78964d5532f694784e70110acf7" + dependencies: + inherits "^2.0.3" + readable-stream "^2.2.2" + typedarray "^0.0.6" + +concurrently@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/concurrently/-/concurrently-3.3.0.tgz#d8eb7a9765fdf0b28d12220dc058e14d03c7dd4f" + dependencies: + chalk "0.5.1" + commander "2.6.0" + date-fns "^1.23.0" + lodash "^4.5.1" + rx "2.3.24" + spawn-command "^0.0.2-1" + supports-color "^3.2.3" + tree-kill "^1.1.0" + +configstore@^1.0.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/configstore/-/configstore-1.4.0.tgz#c35781d0501d268c25c54b8b17f6240e8a4fb021" + dependencies: + graceful-fs "^4.1.2" + mkdirp "^0.5.0" + object-assign "^4.0.1" + os-tmpdir "^1.0.0" + osenv "^0.1.0" + uuid "^2.0.1" + write-file-atomic "^1.1.2" + xdg-basedir "^2.0.0" + +configstore@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/configstore/-/configstore-2.1.0.tgz#737a3a7036e9886102aa6099e47bb33ab1aba1a1" + dependencies: + dot-prop "^3.0.0" + graceful-fs "^4.1.2" + mkdirp "^0.5.0" + object-assign "^4.0.1" + os-tmpdir "^1.0.0" + osenv "^0.1.0" + uuid "^2.0.1" + write-file-atomic "^1.1.2" + xdg-basedir "^2.0.0" + +console-control-strings@^1.0.0, console-control-strings@~1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e" + +content-disposition@0.5.2: + version "0.5.2" + resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.2.tgz#0cf68bb9ddf5f2be7961c3a85178cb85dba78cb4" + +content-type@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.2.tgz#b7d113aee7a8dd27bd21133c4dc2529df1721eed" + +cookie-signature@1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" + +cookie@0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.3.1.tgz#e7e0a1f9ef43b4c8ba925c5c5a96e806d16873bb" + +cookiejar@^2.0.6: + version "2.1.0" + resolved "https://registry.yarnpkg.com/cookiejar/-/cookiejar-2.1.0.tgz#86549689539b6d0e269b6637a304be508194d898" + +core-util-is@~1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" + +create-error-class@^3.0.1: + version "3.0.2" + resolved "https://registry.yarnpkg.com/create-error-class/-/create-error-class-3.0.2.tgz#06be7abef947a3f14a30fd610671d401bca8b7b6" + dependencies: + capture-stack-trace "^1.0.0" + +cryptiles@2.x.x: + version "2.0.5" + resolved "https://registry.yarnpkg.com/cryptiles/-/cryptiles-2.0.5.tgz#3bdfecdc608147c1c67202fa291e7dca59eaa3b8" + dependencies: + boom "2.x.x" + +d@^0.1.1, d@~0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/d/-/d-0.1.1.tgz#da184c535d18d8ee7ba2aa229b914009fae11309" + dependencies: + es5-ext "~0.10.2" + +dashdash@^1.12.0: + version "1.14.1" + resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0" + dependencies: + assert-plus "^1.0.0" + +date-fns@^1.23.0: + version "1.27.2" + resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-1.27.2.tgz#ce82f420bc028356cc661fc55c0494a56a990c9c" + +debug@^2.1.1, debug@^2.2.0, debug@~2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.2.0.tgz#f87057e995b1a1f6ae6a4960664137bc56f039da" + dependencies: + ms "0.7.1" + +deep-extend@~0.4.0: + version "0.4.1" + resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.4.1.tgz#efe4113d08085f4e6f9687759810f807469e2253" + +deep-is@~0.1.3: + version "0.1.3" + resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34" + +del@^2.0.2: + version "2.2.2" + resolved "https://registry.yarnpkg.com/del/-/del-2.2.2.tgz#c12c981d067846c84bcaf862cff930d907ffd1a8" + dependencies: + globby "^5.0.0" + is-path-cwd "^1.0.0" + is-path-in-cwd "^1.0.0" + object-assign "^4.0.1" + pify "^2.0.0" + pinkie-promise "^2.0.0" + rimraf "^2.2.8" + +delayed-stream@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" + +delegates@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" + +depd@~1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.0.tgz#e1bd82c6aab6ced965b97b88b17ed3e528ca18c3" + +destroy@~1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80" + +diff@^3.0.1: + version "3.2.0" + resolved "https://registry.yarnpkg.com/diff/-/diff-3.2.0.tgz#c9ce393a4b7cbd0b058a725c93df299027868ff9" + +doctrine@^1.2.2: + version "1.5.0" + resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-1.5.0.tgz#379dce730f6166f76cefa4e6707a159b02c5a6fa" + dependencies: + esutils "^2.0.2" + isarray "^1.0.0" + +dot-prop@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-3.0.0.tgz#1b708af094a49c9a0e7dbcad790aba539dac1177" + dependencies: + is-obj "^1.0.0" + +duplexer2@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/duplexer2/-/duplexer2-0.1.4.tgz#8b12dab878c0d69e3e7891051662a32fc6bddcc1" + dependencies: + readable-stream "^2.0.2" + +duplexer@~0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.1.tgz#ace6ff808c1ce66b57d1ebf97977acb02334cfc1" + +duplexify@^3.2.0: + version "3.5.0" + resolved "https://registry.yarnpkg.com/duplexify/-/duplexify-3.5.0.tgz#1aa773002e1578457e9d9d4a50b0ccaaebcbd604" + dependencies: + end-of-stream "1.0.0" + inherits "^2.0.1" + readable-stream "^2.0.0" + stream-shift "^1.0.0" + +ecc-jsbn@~0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz#0fc73a9ed5f0d53c38193398523ef7e543777505" + dependencies: + jsbn "~0.1.0" + +ee-first@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" + +encodeurl@~1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.1.tgz#79e3d58655346909fe6f0f45a5de68103b294d20" + +end-of-stream@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.0.0.tgz#d4596e702734a93e40e9af864319eabd99ff2f0e" + dependencies: + once "~1.3.0" + +error-ex@^1.2.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.0.tgz#e67b43f3e82c96ea3a584ffee0b9fc3325d802d9" + dependencies: + is-arrayish "^0.2.1" + +es5-ext@^0.10.7, es5-ext@^0.10.8, es5-ext@~0.10.11, es5-ext@~0.10.2, es5-ext@~0.10.7: + version "0.10.12" + resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.12.tgz#aa84641d4db76b62abba5e45fd805ecbab140047" + dependencies: + es6-iterator "2" + es6-symbol "~3.1" + +es6-iterator@2: + version "2.0.0" + resolved "https://registry.yarnpkg.com/es6-iterator/-/es6-iterator-2.0.0.tgz#bd968567d61635e33c0b80727613c9cb4b096bac" + dependencies: + d "^0.1.1" + es5-ext "^0.10.7" + es6-symbol "3" + +es6-map@^0.1.3: + version "0.1.4" + resolved "https://registry.yarnpkg.com/es6-map/-/es6-map-0.1.4.tgz#a34b147be224773a4d7da8072794cefa3632b897" + dependencies: + d "~0.1.1" + es5-ext "~0.10.11" + es6-iterator "2" + es6-set "~0.1.3" + es6-symbol "~3.1.0" + event-emitter "~0.3.4" + +es6-promise@^3.0.2: + version "3.3.1" + resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-3.3.1.tgz#a08cdde84ccdbf34d027a1451bc91d4bcd28a613" + +es6-set@~0.1.3: + version "0.1.4" + resolved "https://registry.yarnpkg.com/es6-set/-/es6-set-0.1.4.tgz#9516b6761c2964b92ff479456233a247dc707ce8" + dependencies: + d "~0.1.1" + es5-ext "~0.10.11" + es6-iterator "2" + es6-symbol "3" + event-emitter "~0.3.4" + +es6-symbol@3, es6-symbol@~3.1, es6-symbol@~3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/es6-symbol/-/es6-symbol-3.1.0.tgz#94481c655e7a7cad82eba832d97d5433496d7ffa" + dependencies: + d "~0.1.1" + es5-ext "~0.10.11" + +es6-weak-map@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/es6-weak-map/-/es6-weak-map-2.0.1.tgz#0d2bbd8827eb5fb4ba8f97fbfea50d43db21ea81" + dependencies: + d "^0.1.1" + es5-ext "^0.10.8" + es6-iterator "2" + es6-symbol "3" + +escape-html@~1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" + +escape-string-regexp@^1.0.0, escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" + +escope@^3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/escope/-/escope-3.6.0.tgz#e01975e812781a163a6dadfdd80398dc64c889c3" + dependencies: + es6-map "^0.1.3" + es6-weak-map "^2.0.1" + esrecurse "^4.1.0" + estraverse "^4.1.1" + +eslint-plugin-jasmine@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-jasmine/-/eslint-plugin-jasmine-2.2.0.tgz#7135879383c39a667c721d302b9f20f0389543de" + +eslint@^3.15.0: + version "3.15.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-3.15.0.tgz#bdcc6a6c5ffe08160e7b93c066695362a91e30f2" + dependencies: + babel-code-frame "^6.16.0" + chalk "^1.1.3" + concat-stream "^1.4.6" + debug "^2.1.1" + doctrine "^1.2.2" + escope "^3.6.0" + espree "^3.4.0" + estraverse "^4.2.0" + esutils "^2.0.2" + file-entry-cache "^2.0.0" + glob "^7.0.3" + globals "^9.14.0" + ignore "^3.2.0" + imurmurhash "^0.1.4" + inquirer "^0.12.0" + is-my-json-valid "^2.10.0" + is-resolvable "^1.0.0" + js-yaml "^3.5.1" + json-stable-stringify "^1.0.0" + levn "^0.3.0" + lodash "^4.0.0" + mkdirp "^0.5.0" + natural-compare "^1.4.0" + optionator "^0.8.2" + path-is-inside "^1.0.1" + pluralize "^1.2.1" + progress "^1.1.8" + require-uncached "^1.0.2" + shelljs "^0.7.5" + strip-bom "^3.0.0" + strip-json-comments "~2.0.1" + table "^3.7.8" + text-table "~0.2.0" + user-home "^2.0.0" + +espree@^3.4.0: + version "3.4.0" + resolved "https://registry.yarnpkg.com/espree/-/espree-3.4.0.tgz#41656fa5628e042878025ef467e78f125cb86e1d" + dependencies: + acorn "4.0.4" + acorn-jsx "^3.0.0" + +esprima@^3.1.1: + version "3.1.3" + resolved "https://registry.yarnpkg.com/esprima/-/esprima-3.1.3.tgz#fdca51cee6133895e3c88d535ce49dbff62a4633" + +esrecurse@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.1.0.tgz#4713b6536adf7f2ac4f327d559e7756bff648220" + dependencies: + estraverse "~4.1.0" + object-assign "^4.0.1" + +estraverse@^4.1.1, estraverse@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.2.0.tgz#0dee3fed31fcd469618ce7342099fc1afa0bdb13" + +estraverse@~4.1.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.1.1.tgz#f6caca728933a850ef90661d0e17982ba47111a2" + +esutils@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.2.tgz#0abf4f1caa5bcb1f7a9d8acc6dea4faaa04bac9b" + +etag@~1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/etag/-/etag-1.7.0.tgz#03d30b5f67dd6e632d2945d30d6652731a34d5d8" + +event-emitter@~0.3.4: + version "0.3.4" + resolved "https://registry.yarnpkg.com/event-emitter/-/event-emitter-0.3.4.tgz#8d63ddfb4cfe1fae3b32ca265c4c720222080bb5" + dependencies: + d "~0.1.1" + es5-ext "~0.10.7" + +event-stream@~3.3.0: + version "3.3.4" + resolved "https://registry.yarnpkg.com/event-stream/-/event-stream-3.3.4.tgz#4ab4c9a0f5a54db9338b4c34d86bfce8f4b35571" + dependencies: + duplexer "~0.1.1" + from "~0" + map-stream "~0.1.0" + pause-stream "0.0.11" + split "0.3" + stream-combiner "~0.0.4" + through "~2.3.1" + +exit-hook@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/exit-hook/-/exit-hook-1.1.1.tgz#f05ca233b48c05d54fff07765df8507e95c02ff8" + +exit@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/exit/-/exit-0.1.2.tgz#0632638f8d877cc82107d30a0fff1a17cba1cd0c" + +expand-brackets@^0.1.4: + version "0.1.5" + resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-0.1.5.tgz#df07284e342a807cd733ac5af72411e581d1177b" + dependencies: + is-posix-bracket "^0.1.0" + +expand-range@^1.8.1: + version "1.8.2" + resolved "https://registry.yarnpkg.com/expand-range/-/expand-range-1.8.2.tgz#a299effd335fe2721ebae8e257ec79644fc85337" + dependencies: + fill-range "^2.1.0" + +express@^4.14.1: + version "4.14.1" + resolved "https://registry.yarnpkg.com/express/-/express-4.14.1.tgz#646c237f766f148c2120aff073817b9e4d7e0d33" + dependencies: + accepts "~1.3.3" + array-flatten "1.1.1" + content-disposition "0.5.2" + content-type "~1.0.2" + cookie "0.3.1" + cookie-signature "1.0.6" + debug "~2.2.0" + depd "~1.1.0" + encodeurl "~1.0.1" + escape-html "~1.0.3" + etag "~1.7.0" + finalhandler "0.5.1" + fresh "0.3.0" + merge-descriptors "1.0.1" + methods "~1.1.2" + on-finished "~2.3.0" + parseurl "~1.3.1" + path-to-regexp "0.1.7" + proxy-addr "~1.1.3" + qs "6.2.0" + range-parser "~1.2.0" + send "0.14.2" + serve-static "~1.11.2" + type-is "~1.6.14" + utils-merge "1.0.0" + vary "~1.1.0" + +extend@^3.0.0, extend@~3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.0.tgz#5a474353b9f3353ddd8176dfd37b91c83a46f1d4" + +extglob@^0.3.1: + version "0.3.2" + resolved "https://registry.yarnpkg.com/extglob/-/extglob-0.3.2.tgz#2e18ff3d2f49ab2765cec9023f011daa8d8349a1" + dependencies: + is-extglob "^1.0.0" + +extsprintf@1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.0.2.tgz#e1080e0658e300b06294990cc70e1502235fd550" + +fast-levenshtein@~2.0.4: + version "2.0.6" + resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" + +figures@^1.3.5: + version "1.7.0" + resolved "https://registry.yarnpkg.com/figures/-/figures-1.7.0.tgz#cbe1e3affcf1cd44b80cadfed28dc793a9701d2e" + dependencies: + escape-string-regexp "^1.0.5" + object-assign "^4.1.0" + +file-entry-cache@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-2.0.0.tgz#c392990c3e684783d838b8c84a45d8a048458361" + dependencies: + flat-cache "^1.2.1" + object-assign "^4.0.1" + +filename-regex@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/filename-regex/-/filename-regex-2.0.0.tgz#996e3e80479b98b9897f15a8a58b3d084e926775" + +fill-range@^2.1.0: + version "2.2.3" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-2.2.3.tgz#50b77dfd7e469bc7492470963699fe7a8485a723" + dependencies: + is-number "^2.1.0" + isobject "^2.0.0" + randomatic "^1.1.3" + repeat-element "^1.1.2" + repeat-string "^1.5.2" + +filled-array@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/filled-array/-/filled-array-1.1.0.tgz#c3c4f6c663b923459a9aa29912d2d031f1507f84" + +finalhandler@0.5.1: + version "0.5.1" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-0.5.1.tgz#2c400d8d4530935bc232549c5fa385ec07de6fcd" + dependencies: + debug "~2.2.0" + escape-html "~1.0.3" + on-finished "~2.3.0" + statuses "~1.3.1" + unpipe "~1.0.0" + +findup-sync@~0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/findup-sync/-/findup-sync-0.3.0.tgz#37930aa5d816b777c03445e1966cc6790a4c0b16" + dependencies: + glob "~5.0.0" + +flat-cache@^1.2.1: + version "1.2.2" + resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-1.2.2.tgz#fa86714e72c21db88601761ecf2f555d1abc6b96" + dependencies: + circular-json "^0.3.1" + del "^2.0.2" + graceful-fs "^4.1.2" + write "^0.2.1" + +for-in@^0.1.5: + version "0.1.6" + resolved "https://registry.yarnpkg.com/for-in/-/for-in-0.1.6.tgz#c9f96e89bfad18a545af5ec3ed352a1d9e5b4dc8" + +for-own@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/for-own/-/for-own-0.1.4.tgz#0149b41a39088c7515f51ebe1c1386d45f935072" + dependencies: + for-in "^0.1.5" + +forever-agent@~0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" + +form-data@^2.1.1, form-data@~2.1.1: + version "2.1.2" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.1.2.tgz#89c3534008b97eada4cbb157d58f6f5df025eae4" + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.5" + mime-types "^2.1.12" + +formidable@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/formidable/-/formidable-1.1.1.tgz#96b8886f7c3c3508b932d6bd70c4d3a88f35f1a9" + +forwarded@~0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.0.tgz#19ef9874c4ae1c297bcf078fde63a09b66a84363" + +fresh@0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.3.0.tgz#651f838e22424e7566de161d8358caa199f83d4f" + +from@~0: + version "0.1.3" + resolved "https://registry.yarnpkg.com/from/-/from-0.1.3.tgz#ef63ac2062ac32acf7862e0d40b44b896f22f3bc" + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + +fsevents@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.1.0.tgz#85195de56ccbc4778da3b3d83d8d1d186eba24ce" + dependencies: + nan "^2.3.0" + node-pre-gyp "^0.6.29" + +fstream-ignore@~1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/fstream-ignore/-/fstream-ignore-1.0.5.tgz#9c31dae34767018fe1d249b24dada67d092da105" + dependencies: + fstream "^1.0.0" + inherits "2" + minimatch "^3.0.0" + +fstream@^1.0.0, fstream@^1.0.2, fstream@~1.0.10: + version "1.0.10" + resolved "https://registry.yarnpkg.com/fstream/-/fstream-1.0.10.tgz#604e8a92fe26ffd9f6fae30399d4984e1ab22822" + dependencies: + graceful-fs "^4.1.2" + inherits "~2.0.0" + mkdirp ">=0.5 0" + rimraf "2" + +gauge@~2.7.1: + version "2.7.3" + resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.3.tgz#1c23855f962f17b3ad3d0dc7443f304542edfe09" + dependencies: + aproba "^1.0.3" + console-control-strings "^1.0.0" + has-unicode "^2.0.0" + object-assign "^4.1.0" + signal-exit "^3.0.0" + string-width "^1.0.1" + strip-ansi "^3.0.1" + wide-align "^1.1.0" + +generate-function@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/generate-function/-/generate-function-2.0.0.tgz#6858fe7c0969b7d4e9093337647ac79f60dfbe74" + +generate-object-property@^1.1.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/generate-object-property/-/generate-object-property-1.2.0.tgz#9c0e1c40308ce804f4783618b937fa88f99d50d0" + dependencies: + is-property "^1.0.0" + +getpass@^0.1.1: + version "0.1.6" + resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.6.tgz#283ffd9fc1256840875311c1b60e8c40187110e6" + dependencies: + assert-plus "^1.0.0" + +glob-base@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/glob-base/-/glob-base-0.3.0.tgz#dbb164f6221b1c0b1ccf82aea328b497df0ea3c4" + dependencies: + glob-parent "^2.0.0" + is-glob "^2.0.0" + +glob-parent@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-2.0.0.tgz#81383d72db054fcccf5336daa902f182f6edbb28" + dependencies: + is-glob "^2.0.0" + +glob@^7.0.0, glob@^7.0.3, glob@^7.0.5, glob@^7.0.6, glob@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.1.tgz#805211df04faaf1c63a3600306cdf5ade50b2ec8" + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.2" + once "^1.3.0" + path-is-absolute "^1.0.0" + +glob@~5.0.0: + version "5.0.15" + resolved "https://registry.yarnpkg.com/glob/-/glob-5.0.15.tgz#1bc936b9e02f4a603fcc222ecf7633d30b8b93b1" + dependencies: + inflight "^1.0.4" + inherits "2" + minimatch "2 || 3" + once "^1.3.0" + path-is-absolute "^1.0.0" + +globals@^9.14.0: + version "9.16.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-9.16.0.tgz#63e903658171ec2d9f51b1d31de5e2b8dc01fb80" + +globby@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/globby/-/globby-5.0.0.tgz#ebd84667ca0dbb330b99bcfc68eac2bc54370e0d" + dependencies: + array-union "^1.0.1" + arrify "^1.0.0" + glob "^7.0.3" + object-assign "^4.0.1" + pify "^2.0.0" + pinkie-promise "^2.0.0" + +got@^3.2.0: + version "3.3.1" + resolved "https://registry.yarnpkg.com/got/-/got-3.3.1.tgz#e5d0ed4af55fc3eef4d56007769d98192bcb2eca" + dependencies: + duplexify "^3.2.0" + infinity-agent "^2.0.0" + is-redirect "^1.0.0" + is-stream "^1.0.0" + lowercase-keys "^1.0.0" + nested-error-stacks "^1.0.0" + object-assign "^3.0.0" + prepend-http "^1.0.0" + read-all-stream "^3.0.0" + timed-out "^2.0.0" + +got@^5.0.0: + version "5.7.1" + resolved "https://registry.yarnpkg.com/got/-/got-5.7.1.tgz#5f81635a61e4a6589f180569ea4e381680a51f35" + dependencies: + create-error-class "^3.0.1" + duplexer2 "^0.1.4" + is-redirect "^1.0.0" + is-retry-allowed "^1.0.0" + is-stream "^1.0.0" + lowercase-keys "^1.0.0" + node-status-codes "^1.0.0" + object-assign "^4.0.1" + parse-json "^2.1.0" + pinkie-promise "^2.0.0" + read-all-stream "^3.0.0" + readable-stream "^2.0.5" + timed-out "^3.0.0" + unzip-response "^1.0.2" + url-parse-lax "^1.0.0" + +graceful-fs@^4.1.11, graceful-fs@^4.1.2: + version "4.1.11" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658" + +"graceful-readlink@>= 1.0.0": + version "1.0.1" + resolved "https://registry.yarnpkg.com/graceful-readlink/-/graceful-readlink-1.0.1.tgz#4cafad76bc62f02fa039b2f94e9a3dd3a391a725" + +har-validator@~2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-2.0.6.tgz#cdcbc08188265ad119b6a5a7c8ab70eecfb5d27d" + dependencies: + chalk "^1.1.1" + commander "^2.9.0" + is-my-json-valid "^2.12.4" + pinkie-promise "^2.0.0" + +has-ansi@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-0.1.0.tgz#84f265aae8c0e6a88a12d7022894b7568894c62e" + dependencies: + ansi-regex "^0.2.0" + +has-ansi@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91" + dependencies: + ansi-regex "^2.0.0" + +has-flag@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-1.0.0.tgz#9d9e793165ce017a00f00418c43f942a7b1d11fa" + +has-unicode@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" + +hawk@~3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/hawk/-/hawk-3.1.3.tgz#078444bd7c1640b0fe540d2c9b73d59678e8e1c4" + dependencies: + boom "2.x.x" + cryptiles "2.x.x" + hoek "2.x.x" + sntp "1.x.x" + +hoek@2.x.x: + version "2.16.3" + resolved "https://registry.yarnpkg.com/hoek/-/hoek-2.16.3.tgz#20bb7403d3cea398e91dc4710a8ff1b8274a25ed" + +http-errors@~1.5.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.5.1.tgz#788c0d2c1de2c81b9e6e8c01843b6b97eb920750" + dependencies: + inherits "2.0.3" + setprototypeof "1.0.2" + statuses ">= 1.3.1 < 2" + +http-signature@~1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.1.1.tgz#df72e267066cd0ac67fb76adf8e134a8fbcf91bf" + dependencies: + assert-plus "^0.2.0" + jsprim "^1.2.2" + sshpk "^1.7.0" + +ignore-by-default@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/ignore-by-default/-/ignore-by-default-1.0.1.tgz#48ca6d72f6c6a3af00a9ad4ae6876be3889e2b09" + +ignore@^3.2.0: + version "3.2.2" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.2.2.tgz#1c51e1ef53bab6ddc15db4d9ac4ec139eceb3410" + +imurmurhash@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" + +infinity-agent@^2.0.0: + version "2.0.3" + resolved "https://registry.yarnpkg.com/infinity-agent/-/infinity-agent-2.0.3.tgz#45e0e2ff7a9eb030b27d62b74b3744b7a7ac4216" + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2, inherits@2.0.3, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.0, inherits@~2.0.1: + version "2.0.3" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" + +ini@~1.3.0: + version "1.3.4" + resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.4.tgz#0537cb79daf59b59a1a517dff706c86ec039162e" + +inquirer@^0.12.0: + version "0.12.0" + resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-0.12.0.tgz#1ef2bfd63504df0bc75785fff8c2c41df12f077e" + dependencies: + ansi-escapes "^1.1.0" + ansi-regex "^2.0.0" + chalk "^1.0.0" + cli-cursor "^1.0.1" + cli-width "^2.0.0" + figures "^1.3.5" + lodash "^4.3.0" + readline2 "^1.0.1" + run-async "^0.1.0" + rx-lite "^3.1.2" + string-width "^1.0.1" + strip-ansi "^3.0.0" + through "^2.3.6" + +interpret@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.0.1.tgz#d579fb7f693b858004947af39fa0db49f795602c" + +ipaddr.js@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.2.0.tgz#8aba49c9192799585bdd643e0ccb50e8ae777ba4" + +is-arrayish@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" + +is-binary-path@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-1.0.1.tgz#75f16642b480f187a711c814161fd3a4a7655898" + dependencies: + binary-extensions "^1.0.0" + +is-buffer@^1.0.2: + version "1.1.4" + resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.4.tgz#cfc86ccd5dc5a52fa80489111c6920c457e2d98b" + +is-dotfile@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-dotfile/-/is-dotfile-1.0.2.tgz#2c132383f39199f8edc268ca01b9b007d205cc4d" + +is-equal-shallow@^0.1.3: + version "0.1.3" + resolved "https://registry.yarnpkg.com/is-equal-shallow/-/is-equal-shallow-0.1.3.tgz#2238098fc221de0bcfa5d9eac4c45d638aa1c534" + dependencies: + is-primitive "^2.0.0" + +is-extendable@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-0.1.1.tgz#62b110e289a471418e3ec36a617d472e301dfc89" + +is-extglob@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-1.0.0.tgz#ac468177c4943405a092fc8f29760c6ffc6206c0" + +is-finite@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-finite/-/is-finite-1.0.2.tgz#cc6677695602be550ef11e8b4aa6305342b6d0aa" + dependencies: + number-is-nan "^1.0.0" + +is-fullwidth-code-point@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz#ef9e31386f031a7f0d643af82fde50c457ef00cb" + dependencies: + number-is-nan "^1.0.0" + +is-fullwidth-code-point@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" + +is-glob@^2.0.0, is-glob@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-2.0.1.tgz#d096f926a3ded5600f3fdfd91198cb0888c2d863" + dependencies: + is-extglob "^1.0.0" + +is-my-json-valid@^2.10.0, is-my-json-valid@^2.12.4: + version "2.15.0" + resolved "https://registry.yarnpkg.com/is-my-json-valid/-/is-my-json-valid-2.15.0.tgz#936edda3ca3c211fd98f3b2d3e08da43f7b2915b" + dependencies: + generate-function "^2.0.0" + generate-object-property "^1.1.0" + jsonpointer "^4.0.0" + xtend "^4.0.0" + +is-npm@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-npm/-/is-npm-1.0.0.tgz#f2fb63a65e4905b406c86072765a1a4dc793b9f4" + +is-number@^2.0.2, is-number@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-2.1.0.tgz#01fcbbb393463a548f2f466cce16dece49db908f" + dependencies: + kind-of "^3.0.2" + +is-obj@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-1.0.1.tgz#3e4729ac1f5fde025cd7d83a896dab9f4f67db0f" + +is-path-cwd@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-path-cwd/-/is-path-cwd-1.0.0.tgz#d225ec23132e89edd38fda767472e62e65f1106d" + +is-path-in-cwd@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-path-in-cwd/-/is-path-in-cwd-1.0.0.tgz#6477582b8214d602346094567003be8a9eac04dc" + dependencies: + is-path-inside "^1.0.0" + +is-path-inside@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-1.0.0.tgz#fc06e5a1683fbda13de667aff717bbc10a48f37f" + dependencies: + path-is-inside "^1.0.1" + +is-posix-bracket@^0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/is-posix-bracket/-/is-posix-bracket-0.1.1.tgz#3334dc79774368e92f016e6fbc0a88f5cd6e6bc4" + +is-primitive@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-primitive/-/is-primitive-2.0.0.tgz#207bab91638499c07b2adf240a41a87210034575" + +is-property@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-property/-/is-property-1.0.2.tgz#57fe1c4e48474edd65b09911f26b1cd4095dda84" + +is-redirect@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-redirect/-/is-redirect-1.0.0.tgz#1d03dded53bd8db0f30c26e4f95d36fc7c87dc24" + +is-resolvable@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-resolvable/-/is-resolvable-1.0.0.tgz#8df57c61ea2e3c501408d100fb013cf8d6e0cc62" + dependencies: + tryit "^1.0.1" + +is-retry-allowed@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-retry-allowed/-/is-retry-allowed-1.1.0.tgz#11a060568b67339444033d0125a61a20d564fb34" + +is-stream@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" + +is-typedarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" + +isarray@1.0.0, isarray@^1.0.0, isarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" + +isobject@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/isobject/-/isobject-2.1.0.tgz#f065561096a3f1da2ef46272f815c840d87e0c89" + dependencies: + isarray "1.0.0" + +isstream@~0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" + +jasmine-core@~2.5.2: + version "2.5.2" + resolved "https://registry.yarnpkg.com/jasmine-core/-/jasmine-core-2.5.2.tgz#6f61bd79061e27f43e6f9355e44b3c6cab6ff297" + +jasmine@^2.5.3: + version "2.5.3" + resolved "https://registry.yarnpkg.com/jasmine/-/jasmine-2.5.3.tgz#5441f254e1fc2269deb1dfd93e0e57d565ff4d22" + dependencies: + exit "^0.1.2" + glob "^7.0.6" + jasmine-core "~2.5.2" + +jodid25519@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/jodid25519/-/jodid25519-1.0.2.tgz#06d4912255093419477d425633606e0e90782967" + dependencies: + jsbn "~0.1.0" + +js-tokens@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.1.tgz#08e9f132484a2c45a30907e9dc4d5567b7f114d7" + +js-yaml@^3.5.1: + version "3.8.1" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.8.1.tgz#782ba50200be7b9e5a8537001b7804db3ad02628" + dependencies: + argparse "^1.0.7" + esprima "^3.1.1" + +jsbn@~0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" + +json-schema@0.2.3: + version "0.2.3" + resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13" + +json-stable-stringify@^1.0.0, json-stable-stringify@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz#9a759d39c5f2ff503fd5300646ed445f88c4f9af" + dependencies: + jsonify "~0.0.0" + +json-stringify-safe@~5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" + +jsonify@~0.0.0: + version "0.0.0" + resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73" + +jsonpointer@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/jsonpointer/-/jsonpointer-4.0.1.tgz#4fd92cb34e0e9db3c89c8622ecf51f9b978c6cb9" + +jsprim@^1.2.2: + version "1.3.1" + resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.3.1.tgz#2a7256f70412a29ee3670aaca625994c4dcff252" + dependencies: + extsprintf "1.0.2" + json-schema "0.2.3" + verror "1.3.6" + +kind-of@^3.0.2: + version "3.1.0" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.1.0.tgz#475d698a5e49ff5e53d14e3e732429dc8bf4cf47" + dependencies: + is-buffer "^1.0.2" + +latest-version@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/latest-version/-/latest-version-1.0.1.tgz#72cfc46e3e8d1be651e1ebb54ea9f6ea96f374bb" + dependencies: + package-json "^1.0.0" + +latest-version@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/latest-version/-/latest-version-2.0.0.tgz#56f8d6139620847b8017f8f1f4d78e211324168b" + dependencies: + package-json "^2.0.0" + +lazy-req@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/lazy-req/-/lazy-req-1.1.0.tgz#bdaebead30f8d824039ce0ce149d4daa07ba1fac" + +levn@^0.3.0, levn@~0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee" + dependencies: + prelude-ls "~1.1.2" + type-check "~0.3.2" + +lodash._baseassign@^3.0.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/lodash._baseassign/-/lodash._baseassign-3.2.0.tgz#8c38a099500f215ad09e59f1722fd0c52bfe0a4e" + dependencies: + lodash._basecopy "^3.0.0" + lodash.keys "^3.0.0" + +lodash._basecopy@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/lodash._basecopy/-/lodash._basecopy-3.0.1.tgz#8da0e6a876cf344c0ad8a54882111dd3c5c7ca36" + +lodash._bindcallback@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/lodash._bindcallback/-/lodash._bindcallback-3.0.1.tgz#e531c27644cf8b57a99e17ed95b35c748789392e" + +lodash._createassigner@^3.0.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/lodash._createassigner/-/lodash._createassigner-3.1.1.tgz#838a5bae2fdaca63ac22dee8e19fa4e6d6970b11" + dependencies: + lodash._bindcallback "^3.0.0" + lodash._isiterateecall "^3.0.0" + lodash.restparam "^3.0.0" + +lodash._getnative@^3.0.0: + version "3.9.1" + resolved "https://registry.yarnpkg.com/lodash._getnative/-/lodash._getnative-3.9.1.tgz#570bc7dede46d61cdcde687d65d3eecbaa3aaff5" + +lodash._isiterateecall@^3.0.0: + version "3.0.9" + resolved "https://registry.yarnpkg.com/lodash._isiterateecall/-/lodash._isiterateecall-3.0.9.tgz#5203ad7ba425fae842460e696db9cf3e6aac057c" + +lodash.assign@^3.0.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/lodash.assign/-/lodash.assign-3.2.0.tgz#3ce9f0234b4b2223e296b8fa0ac1fee8ebca64fa" + dependencies: + lodash._baseassign "^3.0.0" + lodash._createassigner "^3.0.0" + lodash.keys "^3.0.0" + +lodash.defaults@^3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/lodash.defaults/-/lodash.defaults-3.1.2.tgz#c7308b18dbf8bc9372d701a73493c61192bd2e2c" + dependencies: + lodash.assign "^3.0.0" + lodash.restparam "^3.0.0" + +lodash.isarguments@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz#2f573d85c6a24289ff00663b491c1d338ff3458a" + +lodash.isarray@^3.0.0: + version "3.0.4" + resolved "https://registry.yarnpkg.com/lodash.isarray/-/lodash.isarray-3.0.4.tgz#79e4eb88c36a8122af86f844aa9bcd851b5fbb55" + +lodash.keys@^3.0.0: + version "3.1.2" + resolved "https://registry.yarnpkg.com/lodash.keys/-/lodash.keys-3.1.2.tgz#4dbc0472b156be50a0b286855d1bd0b0c656098a" + dependencies: + lodash._getnative "^3.0.0" + lodash.isarguments "^3.0.0" + lodash.isarray "^3.0.0" + +lodash.restparam@^3.0.0: + version "3.6.1" + resolved "https://registry.yarnpkg.com/lodash.restparam/-/lodash.restparam-3.6.1.tgz#936a4e309ef330a7645ed4145986c85ae5b20805" + +lodash@^4.0.0, lodash@^4.3.0, lodash@^4.5.1: + version "4.17.4" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae" + +lowercase-keys@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.0.tgz#4e3366b39e7f5457e35f1324bdf6f88d0bfc7306" + +map-stream@~0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/map-stream/-/map-stream-0.1.0.tgz#e56aa94c4c8055a16404a0674b78f215f7c8e194" + +media-typer@0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" + +merge-descriptors@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" + +methods@^1.1.1, methods@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" + +micromatch@^2.1.5: + version "2.3.11" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-2.3.11.tgz#86677c97d1720b363431d04d0d15293bd38c1565" + dependencies: + arr-diff "^2.0.0" + array-unique "^0.2.1" + braces "^1.8.2" + expand-brackets "^0.1.4" + extglob "^0.3.1" + filename-regex "^2.0.0" + is-extglob "^1.0.0" + is-glob "^2.0.1" + kind-of "^3.0.2" + normalize-path "^2.0.1" + object.omit "^2.0.0" + parse-glob "^3.0.4" + regex-cache "^0.4.2" + +mime-db@~1.26.0: + version "1.26.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.26.0.tgz#eaffcd0e4fc6935cf8134da246e2e6c35305adff" + +mime-types@^2.1.12, mime-types@~2.1.11, mime-types@~2.1.13, mime-types@~2.1.7: + version "2.1.14" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.14.tgz#f7ef7d97583fcaf3b7d282b6f8b5679dab1e94ee" + dependencies: + mime-db "~1.26.0" + +mime@1.3.4, mime@^1.3.4: + version "1.3.4" + resolved "https://registry.yarnpkg.com/mime/-/mime-1.3.4.tgz#115f9e3b6b3daf2959983cb38f149a2d40eb5d53" + +"minimatch@2 || 3", minimatch@^3.0.0, minimatch@^3.0.2: + version "3.0.3" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.3.tgz#2a4e4090b96b2db06a9d7df01055a62a77c9b774" + dependencies: + brace-expansion "^1.0.0" + +minimist@0.0.8, minimist@~0.0.1: + version "0.0.8" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" + +minimist@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" + +"mkdirp@>=0.5 0", mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@~0.5.1: + version "0.5.1" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" + dependencies: + minimist "0.0.8" + +ms@0.7.1: + version "0.7.1" + resolved "https://registry.yarnpkg.com/ms/-/ms-0.7.1.tgz#9cd13c03adbff25b65effde7ce864ee952017098" + +ms@0.7.2: + version "0.7.2" + resolved "https://registry.yarnpkg.com/ms/-/ms-0.7.2.tgz#ae25cf2512b3885a1d95d7f037868d8431124765" + +mute-stream@0.0.5: + version "0.0.5" + resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.5.tgz#8fbfabb0a98a253d3184331f9e8deb7372fac6c0" + +nan@^2.3.0: + version "2.5.1" + resolved "https://registry.yarnpkg.com/nan/-/nan-2.5.1.tgz#d5b01691253326a97a2bbee9e61c55d8d60351e2" + +natural-compare@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" + +negotiator@0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.1.tgz#2b327184e8992101177b28563fb5e7102acd0ca9" + +nested-error-stacks@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/nested-error-stacks/-/nested-error-stacks-1.0.2.tgz#19f619591519f096769a5ba9a86e6eeec823c3cf" + dependencies: + inherits "~2.0.1" + +node-pre-gyp@^0.6.29: + version "0.6.33" + resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.6.33.tgz#640ac55198f6a925972e0c16c4ac26a034d5ecc9" + dependencies: + mkdirp "~0.5.1" + nopt "~3.0.6" + npmlog "^4.0.1" + rc "~1.1.6" + request "^2.79.0" + rimraf "~2.5.4" + semver "~5.3.0" + tar "~2.2.1" + tar-pack "~3.3.0" + +node-status-codes@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/node-status-codes/-/node-status-codes-1.0.0.tgz#5ae5541d024645d32a58fcddc9ceecea7ae3ac2f" + +nodemon@^1.11.0: + version "1.11.0" + resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-1.11.0.tgz#226c562bd2a7b13d3d7518b49ad4828a3623d06c" + dependencies: + chokidar "^1.4.3" + debug "^2.2.0" + es6-promise "^3.0.2" + ignore-by-default "^1.0.0" + lodash.defaults "^3.1.2" + minimatch "^3.0.0" + ps-tree "^1.0.1" + touch "1.0.0" + undefsafe "0.0.3" + update-notifier "0.5.0" + +nopt@~1.0.10: + version "1.0.10" + resolved "https://registry.yarnpkg.com/nopt/-/nopt-1.0.10.tgz#6ddd21bd2a31417b92727dd585f8a6f37608ebee" + dependencies: + abbrev "1" + +nopt@~3.0.6: + version "3.0.6" + resolved "https://registry.yarnpkg.com/nopt/-/nopt-3.0.6.tgz#c6465dbf08abcd4db359317f79ac68a646b28ff9" + dependencies: + abbrev "1" + +normalize-path@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-2.0.1.tgz#47886ac1662760d4261b7d979d241709d3ce3f7a" + +npmlog@^4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.0.2.tgz#d03950e0e78ce1527ba26d2a7592e9348ac3e75f" + dependencies: + are-we-there-yet "~1.1.2" + console-control-strings "~1.1.0" + gauge "~2.7.1" + set-blocking "~2.0.0" + +number-is-nan@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d" + +oauth-sign@~0.8.1: + version "0.8.2" + resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.8.2.tgz#46a6ab7f0aead8deae9ec0565780b7d4efeb9d43" + +object-assign@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-3.0.0.tgz#9bedd5ca0897949bca47e7ff408062d549f587f2" + +object-assign@^4.0.1, object-assign@^4.1.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" + +object.omit@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/object.omit/-/object.omit-2.0.1.tgz#1a9c744829f39dbb858c76ca3579ae2a54ebd1fa" + dependencies: + for-own "^0.1.4" + is-extendable "^0.1.1" + +on-finished@~2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" + dependencies: + ee-first "1.1.1" + +once@^1.3.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + dependencies: + wrappy "1" + +once@~1.3.0, once@~1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/once/-/once-1.3.3.tgz#b2e261557ce4c314ec8304f3fa82663e4297ca20" + dependencies: + wrappy "1" + +onetime@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/onetime/-/onetime-1.1.0.tgz#a1f7838f8314c516f05ecefcbc4ccfe04b4ed789" + +optimist@~0.6.0: + version "0.6.1" + resolved "https://registry.yarnpkg.com/optimist/-/optimist-0.6.1.tgz#da3ea74686fa21a19a111c326e90eb15a0196686" + dependencies: + minimist "~0.0.1" + wordwrap "~0.0.2" + +optionator@^0.8.2: + version "0.8.2" + resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.2.tgz#364c5e409d3f4d6301d6c0b4c05bba50180aeb64" + dependencies: + deep-is "~0.1.3" + fast-levenshtein "~2.0.4" + levn "~0.3.0" + prelude-ls "~1.1.2" + type-check "~0.3.2" + wordwrap "~1.0.0" + +os-homedir@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3" + +os-tmpdir@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" + +osenv@^0.1.0: + version "0.1.4" + resolved "https://registry.yarnpkg.com/osenv/-/osenv-0.1.4.tgz#42fe6d5953df06c8064be6f176c3d05aaaa34644" + dependencies: + os-homedir "^1.0.0" + os-tmpdir "^1.0.0" + +package-json@^1.0.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/package-json/-/package-json-1.2.0.tgz#c8ecac094227cdf76a316874ed05e27cc939a0e0" + dependencies: + got "^3.2.0" + registry-url "^3.0.0" + +package-json@^2.0.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/package-json/-/package-json-2.4.0.tgz#0d15bd67d1cbbddbb2ca222ff2edb86bcb31a8bb" + dependencies: + got "^5.0.0" + registry-auth-token "^3.0.1" + registry-url "^3.0.3" + semver "^5.1.0" + +parse-glob@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/parse-glob/-/parse-glob-3.0.4.tgz#b2c376cfb11f35513badd173ef0bb6e3a388391c" + dependencies: + glob-base "^0.3.0" + is-dotfile "^1.0.0" + is-extglob "^1.0.0" + is-glob "^2.0.0" + +parse-json@^2.1.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-2.2.0.tgz#f480f40434ef80741f8469099f8dea18f55a4dc9" + dependencies: + error-ex "^1.2.0" + +parseurl@~1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.1.tgz#c8ab8c9223ba34888aa64a297b28853bec18da56" + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + +path-is-inside@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/path-is-inside/-/path-is-inside-1.0.2.tgz#365417dede44430d1c11af61027facf074bdfc53" + +path-to-regexp@0.1.7: + version "0.1.7" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" + +pause-stream@0.0.11: + version "0.0.11" + resolved "https://registry.yarnpkg.com/pause-stream/-/pause-stream-0.0.11.tgz#fe5a34b0cbce12b5aa6a2b403ee2e73b602f1445" + dependencies: + through "~2.3" + +pify@^2.0.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" + +pinkie-promise@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/pinkie-promise/-/pinkie-promise-2.0.1.tgz#2135d6dfa7a358c069ac9b178776288228450ffa" + dependencies: + pinkie "^2.0.0" + +pinkie@^2.0.0: + version "2.0.4" + resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870" + +pluralize@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-1.2.1.tgz#d1a21483fd22bb41e58a12fa3421823140897c45" + +prelude-ls@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" + +prepend-http@^1.0.0, prepend-http@^1.0.1: + version "1.0.4" + resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-1.0.4.tgz#d4f4562b0ce3696e41ac52d0e002e57a635dc6dc" + +preserve@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/preserve/-/preserve-0.2.0.tgz#815ed1f6ebc65926f865b310c0713bcb3315ce4b" + +process-nextick-args@~1.0.6: + version "1.0.7" + resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-1.0.7.tgz#150e20b756590ad3f91093f25a4f2ad8bff30ba3" + +progress@^1.1.8: + version "1.1.8" + resolved "https://registry.yarnpkg.com/progress/-/progress-1.1.8.tgz#e260c78f6161cdd9b0e56cc3e0a85de17c7a57be" + +proxy-addr@~1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-1.1.3.tgz#dc97502f5722e888467b3fa2297a7b1ff47df074" + dependencies: + forwarded "~0.1.0" + ipaddr.js "1.2.0" + +ps-tree@^1.0.1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/ps-tree/-/ps-tree-1.1.0.tgz#b421b24140d6203f1ed3c76996b4427b08e8c014" + dependencies: + event-stream "~3.3.0" + +punycode@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" + +qs@6.2.0, qs@^6.1.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.2.0.tgz#3b7848c03c2dece69a9522b0fae8c4126d745f3b" + +qs@~6.3.0: + version "6.3.1" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.3.1.tgz#918c0b3bcd36679772baf135b1acb4c1651ed79d" + +randomatic@^1.1.3: + version "1.1.6" + resolved "https://registry.yarnpkg.com/randomatic/-/randomatic-1.1.6.tgz#110dcabff397e9dcff7c0789ccc0a49adf1ec5bb" + dependencies: + is-number "^2.0.2" + kind-of "^3.0.2" + +range-parser@~1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.0.tgz#f49be6b487894ddc40dcc94a322f611092e00d5e" + +rc@^1.0.1, rc@^1.1.6, rc@~1.1.6: + version "1.1.7" + resolved "https://registry.yarnpkg.com/rc/-/rc-1.1.7.tgz#c5ea564bb07aff9fd3a5b32e906c1d3a65940fea" + dependencies: + deep-extend "~0.4.0" + ini "~1.3.0" + minimist "^1.2.0" + strip-json-comments "~2.0.1" + +read-all-stream@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/read-all-stream/-/read-all-stream-3.1.0.tgz#35c3e177f2078ef789ee4bfafa4373074eaef4fa" + dependencies: + pinkie-promise "^2.0.0" + readable-stream "^2.0.0" + +readable-stream@^2.0.0, "readable-stream@^2.0.0 || ^1.1.13", readable-stream@^2.0.2, readable-stream@^2.0.5, readable-stream@^2.2.2: + version "2.2.2" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.2.2.tgz#a9e6fec3c7dda85f8bb1b3ba7028604556fc825e" + dependencies: + buffer-shims "^1.0.0" + core-util-is "~1.0.0" + inherits "~2.0.1" + isarray "~1.0.0" + process-nextick-args "~1.0.6" + string_decoder "~0.10.x" + util-deprecate "~1.0.1" + +readable-stream@~2.1.4: + version "2.1.5" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.1.5.tgz#66fa8b720e1438b364681f2ad1a63c618448c9d0" + dependencies: + buffer-shims "^1.0.0" + core-util-is "~1.0.0" + inherits "~2.0.1" + isarray "~1.0.0" + process-nextick-args "~1.0.6" + string_decoder "~0.10.x" + util-deprecate "~1.0.1" + +readdirp@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-2.1.0.tgz#4ed0ad060df3073300c48440373f72d1cc642d78" + dependencies: + graceful-fs "^4.1.2" + minimatch "^3.0.2" + readable-stream "^2.0.2" + set-immediate-shim "^1.0.1" + +readline2@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/readline2/-/readline2-1.0.1.tgz#41059608ffc154757b715d9989d199ffbf372e35" + dependencies: + code-point-at "^1.0.0" + is-fullwidth-code-point "^1.0.0" + mute-stream "0.0.5" + +rechoir@^0.6.2: + version "0.6.2" + resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.6.2.tgz#85204b54dba82d5742e28c96756ef43af50e3384" + dependencies: + resolve "^1.1.6" + +regex-cache@^0.4.2: + version "0.4.3" + resolved "https://registry.yarnpkg.com/regex-cache/-/regex-cache-0.4.3.tgz#9b1a6c35d4d0dfcef5711ae651e8e9d3d7114145" + dependencies: + is-equal-shallow "^0.1.3" + is-primitive "^2.0.0" + +registry-auth-token@^3.0.1: + version "3.1.0" + resolved "https://registry.yarnpkg.com/registry-auth-token/-/registry-auth-token-3.1.0.tgz#997c08256e0c7999837b90e944db39d8a790276b" + dependencies: + rc "^1.1.6" + +registry-url@^3.0.0, registry-url@^3.0.3: + version "3.1.0" + resolved "https://registry.yarnpkg.com/registry-url/-/registry-url-3.1.0.tgz#3d4ef870f73dde1d77f0cf9a381432444e174942" + dependencies: + rc "^1.0.1" + +repeat-element@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.2.tgz#ef089a178d1483baae4d93eb98b4f9e4e11d990a" + +repeat-string@^1.5.2: + version "1.6.1" + resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637" + +repeating@^1.1.2: + version "1.1.3" + resolved "https://registry.yarnpkg.com/repeating/-/repeating-1.1.3.tgz#3d4114218877537494f97f77f9785fab810fa4ac" + dependencies: + is-finite "^1.0.0" + +repeating@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/repeating/-/repeating-2.0.1.tgz#5214c53a926d3552707527fbab415dbc08d06dda" + dependencies: + is-finite "^1.0.0" + +request@^2.79.0: + version "2.79.0" + resolved "https://registry.yarnpkg.com/request/-/request-2.79.0.tgz#4dfe5bf6be8b8cdc37fcf93e04b65577722710de" + dependencies: + aws-sign2 "~0.6.0" + aws4 "^1.2.1" + caseless "~0.11.0" + combined-stream "~1.0.5" + extend "~3.0.0" + forever-agent "~0.6.1" + form-data "~2.1.1" + har-validator "~2.0.6" + hawk "~3.1.3" + http-signature "~1.1.0" + is-typedarray "~1.0.0" + isstream "~0.1.2" + json-stringify-safe "~5.0.1" + mime-types "~2.1.7" + oauth-sign "~0.8.1" + qs "~6.3.0" + stringstream "~0.0.4" + tough-cookie "~2.3.0" + tunnel-agent "~0.4.1" + uuid "^3.0.0" + +require-uncached@^1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/require-uncached/-/require-uncached-1.0.3.tgz#4e0d56d6c9662fd31e43011c4b95aa49955421d3" + dependencies: + caller-path "^0.1.0" + resolve-from "^1.0.0" + +resolve-from@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-1.0.1.tgz#26cbfe935d1aeeeabb29bc3fe5aeb01e93d44226" + +resolve@^1.1.6, resolve@^1.1.7: + version "1.2.0" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.2.0.tgz#9589c3f2f6149d1417a40becc1663db6ec6bc26c" + +restore-cursor@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-1.0.1.tgz#34661f46886327fed2991479152252df92daa541" + dependencies: + exit-hook "^1.0.0" + onetime "^1.0.0" + +rimraf@2, rimraf@^2.2.8, rimraf@~2.5.1, rimraf@~2.5.4: + version "2.5.4" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.5.4.tgz#96800093cbf1a0c86bd95b4625467535c29dfa04" + dependencies: + glob "^7.0.5" + +run-async@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/run-async/-/run-async-0.1.0.tgz#c8ad4a5e110661e402a7d21b530e009f25f8e389" + dependencies: + once "^1.3.0" + +rx-lite@^3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/rx-lite/-/rx-lite-3.1.2.tgz#19ce502ca572665f3b647b10939f97fd1615f102" + +rx@2.3.24: + version "2.3.24" + resolved "https://registry.yarnpkg.com/rx/-/rx-2.3.24.tgz#14f950a4217d7e35daa71bbcbe58eff68ea4b2b7" + +semver-diff@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/semver-diff/-/semver-diff-2.1.0.tgz#4bbb8437c8d37e4b0cf1a68fd726ec6d645d6d36" + dependencies: + semver "^5.0.3" + +semver@^5.0.3, semver@^5.1.0, semver@~5.3.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.3.0.tgz#9b2ce5d3de02d17c6012ad326aa6b4d0cf54f94f" + +send@0.14.2: + version "0.14.2" + resolved "https://registry.yarnpkg.com/send/-/send-0.14.2.tgz#39b0438b3f510be5dc6f667a11f71689368cdeef" + dependencies: + debug "~2.2.0" + depd "~1.1.0" + destroy "~1.0.4" + encodeurl "~1.0.1" + escape-html "~1.0.3" + etag "~1.7.0" + fresh "0.3.0" + http-errors "~1.5.1" + mime "1.3.4" + ms "0.7.2" + on-finished "~2.3.0" + range-parser "~1.2.0" + statuses "~1.3.1" + +serve-static@~1.11.2: + version "1.11.2" + resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.11.2.tgz#2cf9889bd4435a320cc36895c9aa57bd662e6ac7" + dependencies: + encodeurl "~1.0.1" + escape-html "~1.0.3" + parseurl "~1.3.1" + send "0.14.2" + +set-blocking@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" + +set-immediate-shim@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz#4b2b1b27eb808a9f8dcc481a58e5e56f599f3f61" + +setprototypeof@1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.0.2.tgz#81a552141ec104b88e89ce383103ad5c66564d08" + +shelljs@^0.7.5, shelljs@^0.7.6: + version "0.7.6" + resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.7.6.tgz#379cccfb56b91c8601e4793356eb5382924de9ad" + dependencies: + glob "^7.0.0" + interpret "^1.0.0" + rechoir "^0.6.2" + +signal-exit@^3.0.0: + version "3.0.2" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d" + +slice-ansi@0.0.4: + version "0.0.4" + resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-0.0.4.tgz#edbf8903f66f7ce2f8eafd6ceed65e264c831b35" + +slide@^1.1.5: + version "1.1.6" + resolved "https://registry.yarnpkg.com/slide/-/slide-1.1.6.tgz#56eb027d65b4d2dce6cb2e2d32c4d4afc9e1d707" + +sntp@1.x.x: + version "1.0.9" + resolved "https://registry.yarnpkg.com/sntp/-/sntp-1.0.9.tgz#6541184cc90aeea6c6e7b35e2659082443c66198" + dependencies: + hoek "2.x.x" + +spawn-command@^0.0.2-1: + version "0.0.2" + resolved "https://registry.yarnpkg.com/spawn-command/-/spawn-command-0.0.2.tgz#9544e1a43ca045f8531aac1a48cb29bdae62338e" + +split@0.3: + version "0.3.3" + resolved "https://registry.yarnpkg.com/split/-/split-0.3.3.tgz#cd0eea5e63a211dfff7eb0f091c4133e2d0dd28f" + dependencies: + through "2" + +sprintf-js@~1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" + +sshpk@^1.7.0: + version "1.10.2" + resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.10.2.tgz#d5a804ce22695515638e798dbe23273de070a5fa" + dependencies: + asn1 "~0.2.3" + assert-plus "^1.0.0" + dashdash "^1.12.0" + getpass "^0.1.1" + optionalDependencies: + bcrypt-pbkdf "^1.0.0" + ecc-jsbn "~0.1.1" + jodid25519 "^1.0.0" + jsbn "~0.1.0" + tweetnacl "~0.14.0" + +"statuses@>= 1.3.1 < 2", statuses@~1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.3.1.tgz#faf51b9eb74aaef3b3acf4ad5f61abf24cb7b93e" + +stream-combiner@~0.0.4: + version "0.0.4" + resolved "https://registry.yarnpkg.com/stream-combiner/-/stream-combiner-0.0.4.tgz#4d5e433c185261dde623ca3f44c586bcf5c4ad14" + dependencies: + duplexer "~0.1.1" + +stream-shift@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.0.tgz#d5c752825e5367e786f78e18e445ea223a155952" + +string-length@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/string-length/-/string-length-1.0.1.tgz#56970fb1c38558e9e70b728bf3de269ac45adfac" + dependencies: + strip-ansi "^3.0.0" + +string-width@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3" + dependencies: + code-point-at "^1.0.0" + is-fullwidth-code-point "^1.0.0" + strip-ansi "^3.0.0" + +string-width@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.0.0.tgz#635c5436cc72a6e0c387ceca278d4e2eec52687e" + dependencies: + is-fullwidth-code-point "^2.0.0" + strip-ansi "^3.0.0" + +string_decoder@~0.10.x: + version "0.10.31" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94" + +stringstream@~0.0.4: + version "0.0.5" + resolved "https://registry.yarnpkg.com/stringstream/-/stringstream-0.0.5.tgz#4e484cd4de5a0bbbee18e46307710a8a81621878" + +strip-ansi@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-0.3.0.tgz#25f48ea22ca79187f3174a4db8759347bb126220" + dependencies: + ansi-regex "^0.2.1" + +strip-ansi@^3.0.0, strip-ansi@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" + dependencies: + ansi-regex "^2.0.0" + +strip-bom@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" + +strip-json-comments@~2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" + +superagent@^3.0.0: + version "3.4.4" + resolved "https://registry.yarnpkg.com/superagent/-/superagent-3.4.4.tgz#85d1d9a8f439ed0fb5c00accf4e029417a117195" + dependencies: + component-emitter "^1.2.0" + cookiejar "^2.0.6" + debug "^2.2.0" + extend "^3.0.0" + form-data "^2.1.1" + formidable "^1.1.1" + methods "^1.1.1" + mime "^1.3.4" + qs "^6.1.0" + readable-stream "^2.0.5" + +supertest@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/supertest/-/supertest-3.0.0.tgz#8d4bb68fd1830ee07033b1c5a5a9a4021c965296" + dependencies: + methods "~1.1.2" + superagent "^3.0.0" + +supports-color@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-0.2.0.tgz#d92de2694eb3f67323973d7ae3d8b55b4c22190a" + +supports-color@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" + +supports-color@^3.2.3: + version "3.2.3" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-3.2.3.tgz#65ac0504b3954171d8a64946b2ae3cbb8a5f54f6" + dependencies: + has-flag "^1.0.0" + +table@^3.7.8: + version "3.8.3" + resolved "https://registry.yarnpkg.com/table/-/table-3.8.3.tgz#2bbc542f0fda9861a755d3947fefd8b3f513855f" + dependencies: + ajv "^4.7.0" + ajv-keywords "^1.0.0" + chalk "^1.1.1" + lodash "^4.0.0" + slice-ansi "0.0.4" + string-width "^2.0.0" + +tar-pack@~3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/tar-pack/-/tar-pack-3.3.0.tgz#30931816418f55afc4d21775afdd6720cee45dae" + dependencies: + debug "~2.2.0" + fstream "~1.0.10" + fstream-ignore "~1.0.5" + once "~1.3.3" + readable-stream "~2.1.4" + rimraf "~2.5.1" + tar "~2.2.1" + uid-number "~0.0.6" + +tar@~2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/tar/-/tar-2.2.1.tgz#8e4d2a256c0e2185c6b18ad694aec968b83cb1d1" + dependencies: + block-stream "*" + fstream "^1.0.2" + inherits "2" + +text-table@~0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" + +through@2, through@^2.3.6, through@~2.3, through@~2.3.1: + version "2.3.8" + resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" + +timed-out@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/timed-out/-/timed-out-2.0.0.tgz#f38b0ae81d3747d628001f41dafc652ace671c0a" + +timed-out@^3.0.0: + version "3.1.3" + resolved "https://registry.yarnpkg.com/timed-out/-/timed-out-3.1.3.tgz#95860bfcc5c76c277f8f8326fd0f5b2e20eba217" + +touch@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/touch/-/touch-1.0.0.tgz#449cbe2dbae5a8c8038e30d71fa0ff464947c4de" + dependencies: + nopt "~1.0.10" + +tough-cookie@~2.3.0: + version "2.3.2" + resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.3.2.tgz#f081f76e4c85720e6c37a5faced737150d84072a" + dependencies: + punycode "^1.4.1" + +tree-kill@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/tree-kill/-/tree-kill-1.1.0.tgz#c963dcf03722892ec59cba569e940b71954d1729" + +tryit@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/tryit/-/tryit-1.0.3.tgz#393be730a9446fd1ead6da59a014308f36c289cb" + +tslint@^4.4.2: + version "4.4.2" + resolved "https://registry.yarnpkg.com/tslint/-/tslint-4.4.2.tgz#b14cb79ae039c72471ab4c2627226b940dda19c6" + dependencies: + babel-code-frame "^6.20.0" + colors "^1.1.2" + diff "^3.0.1" + findup-sync "~0.3.0" + glob "^7.1.1" + optimist "~0.6.0" + resolve "^1.1.7" + update-notifier "^1.0.2" + +tunnel-agent@~0.4.1: + version "0.4.3" + resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.4.3.tgz#6373db76909fe570e08d73583365ed828a74eeeb" + +tweetnacl@^0.14.3, tweetnacl@~0.14.0: + version "0.14.5" + resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" + +type-check@~0.3.2: + version "0.3.2" + resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72" + dependencies: + prelude-ls "~1.1.2" + +type-is@~1.6.14: + version "1.6.14" + resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.14.tgz#e219639c17ded1ca0789092dd54a03826b817cb2" + dependencies: + media-typer "0.3.0" + mime-types "~2.1.13" + +typedarray@^0.0.6: + version "0.0.6" + resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" + +typescript@>=2.1.4, typescript@^2.1.6: + version "2.2.0" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.2.0.tgz#626f2fc70087d2480f21ebb12c1888288c8614e3" + +uid-number@~0.0.6: + version "0.0.6" + resolved "https://registry.yarnpkg.com/uid-number/-/uid-number-0.0.6.tgz#0ea10e8035e8eb5b8e4449f06da1c730663baa81" + +undefsafe@0.0.3: + version "0.0.3" + resolved "https://registry.yarnpkg.com/undefsafe/-/undefsafe-0.0.3.tgz#ecca3a03e56b9af17385baac812ac83b994a962f" + +unpipe@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" + +unzip-response@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/unzip-response/-/unzip-response-1.0.2.tgz#b984f0877fc0a89c2c773cc1ef7b5b232b5b06fe" + +update-notifier@0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/update-notifier/-/update-notifier-0.5.0.tgz#07b5dc2066b3627ab3b4f530130f7eddda07a4cc" + dependencies: + chalk "^1.0.0" + configstore "^1.0.0" + is-npm "^1.0.0" + latest-version "^1.0.0" + repeating "^1.1.2" + semver-diff "^2.0.0" + string-length "^1.0.0" + +update-notifier@^1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/update-notifier/-/update-notifier-1.0.3.tgz#8f92c515482bd6831b7c93013e70f87552c7cf5a" + dependencies: + boxen "^0.6.0" + chalk "^1.0.0" + configstore "^2.0.0" + is-npm "^1.0.0" + latest-version "^2.0.0" + lazy-req "^1.1.0" + semver-diff "^2.0.0" + xdg-basedir "^2.0.0" + +url-parse-lax@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/url-parse-lax/-/url-parse-lax-1.0.0.tgz#7af8f303645e9bd79a272e7a14ac68bc0609da73" + dependencies: + prepend-http "^1.0.1" + +user-home@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/user-home/-/user-home-2.0.0.tgz#9c70bfd8169bc1dcbf48604e0f04b8b49cde9e9f" + dependencies: + os-homedir "^1.0.0" + +util-deprecate@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + +utils-merge@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.0.tgz#0294fb922bb9375153541c4f7096231f287c8af8" + +uuid@^2.0.1: + version "2.0.3" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-2.0.3.tgz#67e2e863797215530dff318e5bf9dcebfd47b21a" + +uuid@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.0.1.tgz#6544bba2dfda8c1cf17e629a3a305e2bb1fee6c1" + +vary@~1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.0.tgz#e1e5affbbd16ae768dd2674394b9ad3022653140" + +verror@1.3.6: + version "1.3.6" + resolved "https://registry.yarnpkg.com/verror/-/verror-1.3.6.tgz#cff5df12946d297d2baaefaa2689e25be01c005c" + dependencies: + extsprintf "1.0.2" + +wide-align@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.0.tgz#40edde802a71fea1f070da3e62dcda2e7add96ad" + dependencies: + string-width "^1.0.1" + +widest-line@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/widest-line/-/widest-line-1.0.0.tgz#0c09c85c2a94683d0d7eaf8ee097d564bf0e105c" + dependencies: + string-width "^1.0.1" + +wordwrap@~0.0.2: + version "0.0.3" + resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.3.tgz#a3d5da6cd5c0bc0008d37234bbaf1bed63059107" + +wordwrap@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + +write-file-atomic@^1.1.2: + version "1.3.1" + resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-1.3.1.tgz#7d45ba32316328dd1ec7d90f60ebc0d845bb759a" + dependencies: + graceful-fs "^4.1.11" + imurmurhash "^0.1.4" + slide "^1.1.5" + +write@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/write/-/write-0.2.1.tgz#5fc03828e264cea3fe91455476f7a3c566cb0757" + dependencies: + mkdirp "^0.5.1" + +xdg-basedir@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-2.0.0.tgz#edbc903cc385fc04523d966a335504b5504d1bd2" + dependencies: + os-homedir "^1.0.0" + +xtend@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af" diff --git a/aio/aio-builds-setup/dockerbuild/scripts-sh/clean-up.sh b/aio/aio-builds-setup/dockerbuild/scripts-sh/clean-up.sh new file mode 100755 index 0000000000..0abd445d9a --- /dev/null +++ b/aio/aio-builds-setup/dockerbuild/scripts-sh/clean-up.sh @@ -0,0 +1,4 @@ +#!/bin/bash +set -e -o pipefail + +node $AIO_SCRIPTS_JS_DIR/dist/lib/clean-up >> /var/log/aio/clean-up.log 2>&1 diff --git a/aio/aio-builds-setup/dockerbuild/scripts-sh/health-check.sh b/aio/aio-builds-setup/dockerbuild/scripts-sh/health-check.sh new file mode 100644 index 0000000000..ec96ef58fc --- /dev/null +++ b/aio/aio-builds-setup/dockerbuild/scripts-sh/health-check.sh @@ -0,0 +1,43 @@ +#!/bin/bash +set +e -o pipefail + + +# Variables +exitCode=0 + + +# Helpers +function reportStatus { + local lastExitCode=$? + echo "$1: $([[ $lastExitCode -eq 0 ]] && echo OK || echo NOT OK)" + [[ $lastExitCode -eq 0 ]] || exitCode=1 +} + + +# Check services +services=( + rsyslog + cron + nginx + pm2 +) +for s in ${services[@]}; do + service $s status > /dev/null + reportStatus "Service '$s'" +done + + +# Check servers +origins=( + http://$AIO_UPLOAD_HOSTNAME:$AIO_UPLOAD_PORT + http://$AIO_NGINX_HOSTNAME:$AIO_NGINX_PORT_HTTP + https://$AIO_NGINX_HOSTNAME:$AIO_NGINX_PORT_HTTPS +) +for o in ${origins[@]}; do + curl --fail --silent $o/health-check > /dev/null + reportStatus "Server '$o'" +done + + +# Exit +exit $exitCode diff --git a/aio/aio-builds-setup/dockerbuild/scripts-sh/init.sh b/aio/aio-builds-setup/dockerbuild/scripts-sh/init.sh new file mode 100755 index 0000000000..5666a9d70a --- /dev/null +++ b/aio/aio-builds-setup/dockerbuild/scripts-sh/init.sh @@ -0,0 +1,15 @@ +#!/bin/bash +set -e -o pipefail + +exec >> /var/log/aio/init.log +exec 2>&1 + +# Start the services +echo [`date`] - Starting services... +service rsyslog start +service cron start +service dnsmasq start +service nginx start +service pm2 start +aio-upload-server-prod start +echo [`date`] - Services started successfully. diff --git a/aio/aio-builds-setup/dockerbuild/scripts-sh/upload-server-prod.sh b/aio/aio-builds-setup/dockerbuild/scripts-sh/upload-server-prod.sh new file mode 100755 index 0000000000..ca05dce584 --- /dev/null +++ b/aio/aio-builds-setup/dockerbuild/scripts-sh/upload-server-prod.sh @@ -0,0 +1,10 @@ +#!/bin/bash +set -e -o pipefail + +# TODO(gkalpak): Ideally, the upload server should be run as a non-privileged user. +# (Currently, there doesn't seem to be a straight forward way.) +action=$([ "$1" == "stop" ] && echo "stop" || echo "start") +pm2 $action $AIO_SCRIPTS_JS_DIR/dist/lib/upload-server \ + --log /var/log/aio/upload-server-prod.log \ + --name aio-upload-server-prod \ + ${@:2} diff --git a/aio/aio-builds-setup/dockerbuild/scripts-sh/upload-server-test.sh b/aio/aio-builds-setup/dockerbuild/scripts-sh/upload-server-test.sh new file mode 100644 index 0000000000..f7d93f14f2 --- /dev/null +++ b/aio/aio-builds-setup/dockerbuild/scripts-sh/upload-server-test.sh @@ -0,0 +1,23 @@ +#!/bin/bash +set -e -o pipefail + +# Set up env variables for testing +export AIO_BUILDS_DIR=$TEST_AIO_BUILDS_DIR +export AIO_GITHUB_TOKEN=$TEST_AIO_GITHUB_TOKEN +export AIO_REPO_SLUG=$TEST_AIO_REPO_SLUG +export AIO_UPLOAD_HOSTNAME=$TEST_AIO_UPLOAD_HOSTNAME +export AIO_UPLOAD_PORT=$TEST_AIO_UPLOAD_PORT + +# Start the upload-server instance +# TODO(gkalpak): Ideally, the upload server should be run as a non-privileged user. +# (Currently, there doesn't seem to be a straight forward way.) +appName=aio-upload-server-test +if [[ "$1" == "stop" ]]; then + pm2 delete $appName +else + pm2 start $AIO_SCRIPTS_JS_DIR/dist/lib/upload-server \ + --log /var/log/aio/upload-server-test.log \ + --name $appName \ + --no-autorestart \ + ${@:2} +fi diff --git a/aio/aio-builds-setup/dockerbuild/scripts-sh/verify-setup.sh b/aio/aio-builds-setup/dockerbuild/scripts-sh/verify-setup.sh new file mode 100644 index 0000000000..27ccd617b9 --- /dev/null +++ b/aio/aio-builds-setup/dockerbuild/scripts-sh/verify-setup.sh @@ -0,0 +1,39 @@ +#!/bin/bash +set -e -o pipefail + +logFile=/var/log/aio/verify-setup.log + +exec 3>&1 +exec >> $logFile +exec 2>&1 + +echo "[`date`] - Starting verification..." + +# Helpers +function countdown { + message=$1 + secs=$2 + while [ $secs -gt 0 ]; do + echo -ne "$message in $secs...\033[0K\r" + sleep 1 + : $((secs--)) + done + echo -ne "\033[0K\r" +} + +function onExit { + aio-upload-server-test stop + echo -e "Full logs in '$logFile'.\n" > /dev/fd/3 +} + +# Setup EXIT trap +trap 'onExit' EXIT + +# Start an upload-server instance for testing +aio-upload-server-test start --log $logFile + +# Give the upload-server some time to start :( +countdown "Starting" 5 > /dev/fd/3 + +# Run the tests +node $AIO_SCRIPTS_JS_DIR/dist/lib/verify-setup | tee /dev/fd/3 diff --git a/aio/aio-builds-setup/docs/01. VM setup - Set up docker.md b/aio/aio-builds-setup/docs/01. VM setup - Set up docker.md new file mode 100644 index 0000000000..9789380243 --- /dev/null +++ b/aio/aio-builds-setup/docs/01. VM setup - Set up docker.md @@ -0,0 +1,35 @@ +# VM Setup - Set up docker + + +## Install docker + +_Debian (jessie):_ +- `sudo apt-get update` +- `sudo apt-get install -y apt-transport-https ca-certificates curl git software-properties-common` +- `curl -fsSL https://apt.dockerproject.org/gpg | sudo apt-key add -` +- `apt-key fingerprint 58118E89F3A912897C070ADBF76221572C52609D` +- `sudo add-apt-repository "deb https://apt.dockerproject.org/repo/ debian-$(lsb_release -cs) main"` +- `sudo apt-get update` +- `sudo apt-get -y install docker-engine` + +_Ubuntu (16.04):_ +- `sudo apt-get update` +- `sudo apt-get install -y curl git linux-image-extra-$(uname -r) linux-image-extra-virtual` +- `sudo apt-get install -y apt-transport-https ca-certificates` +- `curl -fsSL https://yum.dockerproject.org/gpg | sudo apt-key add -` +- `apt-key fingerprint 58118E89F3A912897C070ADBF76221572C52609D` +- `sudo add-apt-repository "deb https://apt.dockerproject.org/repo/ ubuntu-$(lsb_release -cs) main"` +- `sudo apt-get update` +- `sudo apt-get -y install docker-engine` + + +## Start the docker +- `sudo service docker start` + + +## Test docker +- `sudo docker run hello-world` + + +## Start docker on boot +- `sudo systemctl enable docker` diff --git a/aio/aio-builds-setup/docs/02. VM setup - Attach persistent disk.md b/aio/aio-builds-setup/docs/02. VM setup - Attach persistent disk.md new file mode 100644 index 0000000000..c18c6f5e74 --- /dev/null +++ b/aio/aio-builds-setup/docs/02. VM setup - Attach persistent disk.md @@ -0,0 +1,17 @@ +# VM setup - Attach persistent disk + + +## Create `aio-builds` persistent disk (if not already exists) +- Follow instructions [here](https://cloud.google.com/compute/docs/disks/add-persistent-disk#create_disk). +- `sudo mkfs.ext4 -F -E lazy_itable_init=0,lazy_journal_init=0,discard /dev/disk/by-id/google-aio-builds` + + +## Mount disk +- `sudo mkdir -p /mnt/disks/aio-builds` +- `sudo mount -o discard,defaults /dev/disk/by-id/google-aio-builds /mnt/disks/aio-builds` +- `sudo chmod a+w /mnt/disks/aio-builds` + + +## Mount disk on boot +- ``echo UUID=`sudo blkid -s UUID -o value /dev/disk/by-id/google-aio-builds` \ + /mnt/disks/aio-builds ext4 discard,defaults,nofail 0 2 | sudo tee -a /etc/fstab`` diff --git a/aio/aio-builds-setup/docs/NOTES.md b/aio/aio-builds-setup/docs/NOTES.md new file mode 100644 index 0000000000..beb69fe99f --- /dev/null +++ b/aio/aio-builds-setup/docs/NOTES.md @@ -0,0 +1,29 @@ +# VM Setup Instructions + +- Set up docker +- Attach persistent disk +- Build docker image (+ checkout repo) +- Run image (+ setup for run on boot) + + +## Build image +- `/build.sh [[:]]` + + +## Run image +- `sudo docker run \ + -d \ + --dns 127.0.0.1 \ + --name \ + -p 80:80 \ + -p 443:443 \ + -v :/var/www/aio-builds \ + [:] + ` + +## Questions +- Do we care to keep logs (e.g. cron, nginx, aio-upload-server, aio-clean-up, pm2) outside of the container? +- Currently, builds will only be remove when the PR is closed. It is possible to upload arbitrary builds (for non-existent commits) until then. +- Instead of creating new comments for each commit, update the original comment? +- Do we need a static IP address? +- Do we care about persistent disk automatic backup? diff --git a/aio/package.json b/aio/package.json index b1d4db6cef..4fc8211f6c 100644 --- a/aio/package.json +++ b/aio/package.json @@ -14,6 +14,7 @@ "lint": "yarn check-env && ng lint", "pree2e": "webdriver-manager update --standalone false --gecko false", "e2e": "yarn check-env && ng e2e --no-webdriver-update", + "deploy-preview": "scripts/deploy-preview.sh", "deploy-staging": "firebase use staging --token \"$FIREBASE_TOKEN\" && yarn ~~deploy", "pre~~deploy": "yarn build", "~~deploy": "firebase deploy --message \"Commit: $TRAVIS_COMMIT\" --non-interactive --token \"$FIREBASE_TOKEN\"", diff --git a/aio/scripts/deploy-preview.sh b/aio/scripts/deploy-preview.sh new file mode 100755 index 0000000000..dd49776973 --- /dev/null +++ b/aio/scripts/deploy-preview.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash + +set -eux -o pipefail + + +INPUT_DIR=dist/ +OUTPUT_FILE=/tmp/snapshot.tar.gz +AIO_BUILDS_HOST=https://ngbuilds.io + +cd "`dirname $0`/.." +yarn run build -- --prod +tar --create --gzip --directory "$INPUT_DIR" --file "$OUTPUT_FILE" . +curl -iLX POST --header "Authorization: Token $NGBUILDS_IO_KEY" --data-binary "@$OUTPUT_FILE" \ + "$AIO_BUILDS_HOST/create-build/$TRAVIS_PULL_REQUEST/$TRAVIS_COMMIT" +cd - diff --git a/scripts/ci/deploy.sh b/scripts/ci/deploy.sh index db43734066..31f6c77e9d 100755 --- a/scripts/ci/deploy.sh +++ b/scripts/ci/deploy.sh @@ -15,33 +15,53 @@ if [[ ${TRAVIS_TEST_RESULT=0} == 1 ]]; then fi -# Don't deploy if not running against angular/angular and not a PR +# Don't deploy if not running against angular/angular # TODO(i): because we don't let deploy to run outside of angular/angular folks can't use their # private travis build to deploy anywhere. This is likely ok, but this means that @alexeagle's # fancy setup to publish ES2015 packages to github -build repos no longer works. This is ok # since with flat modules we'll have this feature built-in. We should still go and remove # stuff that Alex put in for this from publish-build-artifacts.sh -if [[ ${TRAVIS_REPO_SLUG} != "angular/angular" || ${TRAVIS_PULL_REQUEST} != "false" ]]; then - echo "Skipping deploy to staging because this is a PR build." +if [[ ${TRAVIS_REPO_SLUG} != "angular/angular" ]]; then + echo "Skipping deploy because this is not angular/angular." exit 0 fi case ${CI_MODE} in e2e) + # Don't deploy if this is a PR build + if [[ ${TRAVIS_PULL_REQUEST} != "false" ]]; then + echo "Skipping deploy because this is a PR build." + exit 0 + fi + travisFoldStart "deploy.packages" ${thisDir}/publish-build-artifacts.sh travisFoldEnd "deploy.packages" ;; aio) - # aio deploy is setup only from master to aio-staging.firebaseapp.com for now - if [[ ${TRAVIS_BRANCH} == "master" ]]; then - travisFoldStart "deploy.aio" - ( - cd ${TRAVIS_BUILD_DIR}/aio - yarn run deploy-staging - ) - travisFoldEnd "deploy.aio" + # Don't deploy if this build is not for master + if [[ ${TRAVIS_BRANCH} != "master" ]]; then + echo "Skipping deploy because this build is not for master." + exit 0 fi + + travisFoldStart "deploy.aio" + ( + cd ${TRAVIS_BUILD_DIR}/aio + + if [[ $TRAVIS_PULL_REQUEST != "false" ]]; then + # This is a PR: deploy a snapshot for previewing + travisFoldStart "deploy.aio.pr-preview" + yarn run deploy-preview + travisFoldEnd "deploy.aio.pr-preview" + else + # This is upstream master: Deploy to staging + travisFoldStart "deploy.aio.staging" + yarn run deploy-staging + travisFoldEnd "deploy.aio.staging" + fi + ) + travisFoldEnd "deploy.aio" ;; esac