ci(aio): add initial implementation for aio-builds setup

This commit is contained in:
Georgios Kalpakas
2017-02-06 20:40:28 +02:00
committed by Chuck Jazdzewski
parent 794f8f4e6a
commit 115164033b
49 changed files with 5876 additions and 11 deletions

View File

@ -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<any> {
const prDir = path.join(this.buildsDir, pr);
const shaDir = path.join(prDir, sha);
let dirToRemoveOnError: string;
return Promise.
all([this.exists(prDir), this.exists(shaDir)]).
then(([prDirExisted, shaDirExisted]) => {
if (shaDirExisted) {
throw new UploadError(403, `Request to overwrite existing directory: ${shaDir}`);
}
dirToRemoveOnError = prDirExisted ? shaDir : prDir;
return Promise.resolve().
then(() => shell.mkdir('-p', shaDir)).
then(() => this.extractArchive(archivePath, shaDir)).
then(() => this.emit(CreatedBuildEvent.type, new CreatedBuildEvent(+pr, sha)));
}).
catch(err => {
if (dirToRemoveOnError) {
shell.rm('-rf', dirToRemoveOnError);
}
if (!(err instanceof UploadError)) {
err = new UploadError(500, `Error while uploading to directory: ${shaDir}\n${err}`);
}
throw err;
});
}
// Methods - Protected
protected exists(fileOrDir: string): Promise<boolean> {
return new Promise(resolve => fs.access(fileOrDir, err => resolve(!err)));
}
protected extractArchive(inputFile: string, outputDir: string): Promise<void> {
return new Promise<void>((resolve, reject) => {
const cmd = `tar --extract --gzip --directory "${outputDir}" --file "${inputFile}"`;
cp.exec(cmd, (err, _stdout, stderr) => {
if (err) {
return reject(err);
}
if (stderr) {
console.warn(stderr);
}
try {
// Undocumented signature (see https://github.com/shelljs/shelljs/pull/663).
(shell as any).chmod('-R', 'a-w', outputDir);
shell.rm('-f', inputFile);
resolve();
} catch (err) {
reject(err);
}
});
});
}
}

View File

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

View File

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

View File

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

View File

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